Open jamiebuilds opened 5 years ago
Arrow functions don't have a bound this
. They do not have any bindings and therefore don't have a this
type. They always utilise the this
of the context they are executed in.
That's incorrect, they always utilize the this
of context they are defined in, which is why TypeScript is already able to lookup the this
of arrow functions:
Code flow analysis allows TypeScript to figure out what this
is for an arrow function by looking at the context it is defined in, not because it can actually determine what it is bound to. It is not bound, therefore it doesn't have a this
type. It doesn't "inherit" this
, it uses the this
of the lexical context. It doesn't have its own this
. Per MDN (and many other sources):
An arrow function expression is a syntactically compact alternative to a regular function expression, although without its own bindings to the
this
,arguments
,super
, ornew.target
keywords.
As an aside: I personally find the way arrow functions are talked about as "not binding their own this
" to be confusing, FWIW. Before we had fat arrows, in ES5 we used to go (function() { ... }).bind(this)
to get (roughly) the same result. That's called a bound function, per usual ES terminology. So when I hear "not bound" I automatically think of a regular old function expression that gets its this
as a secret hidden parameter.
As a result of the above I've found it's easier to explain arrow functions to people as being "auto-bound", rather than "not bound". The way the spec (and the documentation surrounding it) is written is very confusing sometimes!
@kitsonk Yes I understand how arrow functions work and I understand how TypeScript inference works. I understand that a this
type in an arrow function doesn't make much sense. However, the bug that I am reporting is that TypeScript does produce a type for this
in an arrow function, and that type is incorrectly.
TypeScript could (and in fact does appear to in some cases) store the this
context for an arrow function from its definition, and then it can later use that this
type to fix this inference bug that I am reporting.
If TypeScript wants to change that type to any
or something, that would also be fine, but right now I am able to produce correct JavaScript code that TypeScript reports as incorrect and this is the underlying cause.
And I don't need you to explain JavaScript to me, I worked on Babel, TC39, and Flow, I gots it
My immediate instinct is that the safe thing to do with arrows would be to treat their this
binding slot as being of type never
. If you know you have an arrow function then it would never make sense to pass in anything meaningful in that position.
However, functions with fewer parameters are purposely assignable to types with more (on the basis that the extra values will simply be ignored), so this: unknown
would probably be more idiomatic.
I was thinking about that. But I think that unknown
would also be incorrect. Think about it this way:
interface MyThis { property: boolean }
function context(this: MyThis) {
let a = () => { console.log(this.property) }
let b = function(this: MyThis) { console.log(this.property) }.bind(this)
}
Should the inferred types of a
and b
be interchangeable (at least in the context of this
)?
I would say yes, they should be.
I would argue that any function returned by .bind()
should also be typed as this: unknown
, for exactly the same reason. So yes, those two cases should in fact be interchangeable, I'll agree with that. :smiley:
this
is simply a special parameter which is (typically) fed by the semantics of the language rather than explicitly by the caller (you know this so I won't go into boring detail and patronize you :). So whether we use an arrow function or .bind()
it away, I’d say it should be treated the same as any other nonexistent parameter for the purpose of assignability.
To expand the original example:
type FunctionThisType<T extends (...args: any[]) => any> =
T extends (this: infer R, ...args: any[]) => any ? R : any
type FunctionArgType<T extends (...args: any[]) => any> =
T extends (...args: [ infer R, ...any[] ]) => any ? R : any
let foo = () => {};
let bar = (x: number) => {};
let quux = () => {};
type FooThis = FunctionThisType<typeof foo>; // {}
type BarArg = FunctionArgType<typeof bar>; // number
type QuuxArg = FunctionArgType<typeof quux>; // {}
For quux
, we can we pass literally anything in as an argument and it will simply be ignored since the function doesn't have a binding slot for it. Therefore, we can assign bar = quux
, even with --strictFunctionTypes
enabled, and the compiler won't complain. this
of arrow functions (and .bind()
ed functions) should be treated the same, IMO.
For quux, we can we pass literally anything in as an argument and it will simply be ignored since the function doesn't have a binding slot for it.
This doesn't really matter, but that's now how .bind()
works:
let a = (...args) => { console.log(...args) })
let b = a.bind(null, 1, 2, 3)
b(4, 5, 6)
// > 1 2 3 4 5 6
All functions in JavaScript, including arrow functions, have a this
parameter that gets captured from different places. Saying
function outer() { return () => console.log(this) }
let inner = outer.call("foo")
inner() // "foo"
The inner
arrow function captures a this
context that it holds onto for its lifetime. To describe the this
as unknown is inaccurate. It is known, it is a very specific thing, and it matters that TypeScript know that inside of the arrow function and outside of it.
function outer1(report) { return () => report(this) }
function outer2(report) { return function(this: unknown) { report(this) }.bind(this) }
To say that these are the same is inaccurate. And I don't think anyone would suggest that they should be the same. So what's different when we are inferring the type of an arrow function, why would we report it differently just because we're outside the arrow function? What values does that provide? The this
isn't private to that function, it's not an implementation detail.
The this
inside an arrow function is just another variable that the function closes over. The this
type of functions is its this
parameter type, i.e. the type you would have to supply as the first argument to .call()
or .apply()
. Arrow functions don't have that parameter, and neither do .bind()
-processed functions (because it's partially-applied away). So TypeScript treats it the same as other nonexistent parameters, i.e. {}
.
To iterate on the .bind()
example: Would you not agree that:
let a = (aa: number, bb: string) => {}; // => void
let b = a.bind(null, 1);
...should return a function of type (bb: string) => void
, because the aa: number
was partially applied away? At one time we knew aa
was a number, but we can no longer glean that information given we only know the type of b
. It's no different with the this
type.
this
isn't quite a parameter, it's very close to one, but it does have distinctions. We would not want to conflate this as parameters = [this, ...arguments]
.
You can always infer types for this
, args
, and the return type. And while ((a, b) => {}).bind(_, a)
transforms the inferred args from [a, b]
to [b]
, you don't transform this
into non-existence, you still have to be able to infer a type, and representing that type as unknown
to signify "non-existence" is changing the meaning of unknown
.
this: t => .bind(t) => ?
parameters: [a, b] => .bind(_, a) => [b]
return: x => n/a => x
It's a weird edge case, but ?
has to be something. Whether that's:
{}
any
unknown
never
T
where T
is the type of this
internallythis
from bound/arrow functions{}
is obviously incorrect, it implies that T
is {}
, but while it could be, that's not why TypeScript is giving it to you.
I would argue that unknown
falls into the same bucket. It implies that T
is unknown
when it is not.
Edge case or not, all I'm saying is that in the following declaration:
function foo(this: MyClass, x: string) {}
MyClass
is the type you're expected to pass in for this
. It says nothing (at least not directly) about the type of this
inside the function. This is the definition the language gives it. If the type you're supposed to use for some parameter doesn't matter because there's no binding for it:
type FirstArgType<T extends (...args: any[]) => any> =
T extends (...args: [ infer R, ...any[] ]) => any ? R : any
type FA = FirstArgType<() => void>; // {}
TypeScript infers that type to be {}
(which is very close to unknown
). This is why you can assign (a: string) => void
to (a: string, b: string) => void
etc., by way of contravariance. (unknown
being the contravariant complement of never
).
Just to give one final counterexample, you would expect the below to work, correct?
class Foo
{
name = "Foo";
blub(callback: (this: Foo, message: string) => void) {
callback.call(this, "glub glub!");
}
}
class Bar
{
name = "Bar";
flub() {
const foo = new Foo();
// This is fine, for obvious reasons:
foo.blub(msg => console.log(`${this.name} says: ${msg}`));
// But this is an error, also for obvious reasons:
foo.blub(function(this: Bar, msg) { console.log(`${this.name} says: ${msg}`); });
}
}
const bar = new Bar();
bar.flub();
Assuming that --strictFunctionTypes
is enabled, because of contravariance, this only works if the this:
type of the arrow function is treated as the top type (unknown
) or something very close to it.
I hope the example above was self-explanatory, but if not, to get back to original example, when you ask the compiler to infer:
FunctionThisType<typeof arrowFn>
The question is “what type do I have to give as the first argument to arrowFn.call()
? And the compiler is essentially telling you:
{}
. It’s an arrow function; you can pass anything you want forthis
. It already has its own.
This is IMO half the reason to use an arrow function at all: a this
will always be passed in implicitly by the semantics of the language, so we set it to a universal input and then ignore it in favor of using our own.
I will say some things that are obvious for the sake of exposition so please don't think I don't think either of you understand how JS works.
There are two interpretation of this
in play:
this
inside the function?The first matter is entirely taken care of because it's only visible inside the function body.
The second matter is what's at stake here and is the only meaningful interpretation of what FunctionThisType
means - given a function that you're not already inside, what is a legal way to call it?
For an arrow function, it doesn't matter what the apparent binding of a call to the function is. When the type of a value doesn't matter, it has historically been {}
, but since the introduction of unknown
, we have generally preferred that type and have taken a few breaking changes (of which this would be one) to move to the more accurate unknown
type.
So I'd be inclined to change this to a slightly more precise unknown
value instead of {}
, because given an arrow function f: () => void
and a value u: unknown
, f.call(u)
is OK.
It implies that
T
isunknown
when it is not.
Nothing isn't unknown
. That is the definition of unknown
- it is the type which all values inhibit.
Concrete example of desirable behavior:
function callWithThis<T extends Function>(fn: T, ths: ThisType<T>) {
fn.call(ths);
}
const arrow = () => 0;
// Should be OK
callWithThis(arrow, null);
TypeScript Version: Version 3.6.0-dev.20190601 Search Terms: This, Arrow, Functions, Infer, Inference, Empty, Object, Incorrect, Any
Code
Expected behavior:
Actual behavior:
Playground Link: [Playground](https://www.typescriptlang.org/play/index.html#src=type%20FunctionThisType%3CT%20extends%20(...args%3A%20any%5B%5D)%20%3D%3E%20any%3E%20%3D%0D%0A%20%20T%20extends%20(this%3A%20infer%20R%2C%20...args%3A%20any%5B%5D)%20%3D%3E%20any%20%3F%20R%20%3A%20any%0D%0A%0D%0Alet%20fn%20%3D%20()%20%3D%3E%20%7B%7D%0D%0Alet%20value%3A%20FunctionThisType%3Ctypeof%20fn%3E%20%3D%20%22wrong%22%0D%0A)
Related Issues: None found.