Typescript: Inferring "this" from arrow function is {}

Created on 1 Jun 2019  路  16Comments  路  Source: microsoft/TypeScript

TypeScript Version: Version 3.6.0-dev.20190601
Search Terms: This, Arrow, Functions, Infer, Inference, Empty, Object, Incorrect, Any

Code

type FunctionThisType<T extends (...args: any[]) => any> =
  T extends (this: infer R, ...args: any[]) => any ? R : any

let fn = () => {}
let value: FunctionThisType<typeof fn> = "wrong"

Expected behavior:

error TS2322: Type '"wrong"' is not assignable to type 'typeof globalThis'.

Found 1 error.

Actual behavior:

No errors found.

Playground Link: Playground

Related Issues: None found.

In Discussion Suggestion

Most helpful comment

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);

All 16 comments

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:

https://www.typescriptlang.org/play/index.html#src=let%20returnsThis%20%3D%20()%20%3D%3E%20this%0D%0Alet%20value%3A%20%22wrong%22%20%3D%20returnsThis()

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, or new.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鈥檇 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 internally
  • A compiler error banning you from inferring this 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 鈥渨hat type do I have to give as the first argument to arrowFn.call()? And the compiler is essentially telling you:

{}. It鈥檚 an arrow function; you can pass anything you want for this. 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:

  • What is the type of the keyword this inside the function?
  • What is a legally-bound invocation of a given 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 is unknown 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);
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

remojansen picture remojansen  路  3Comments

wmaurer picture wmaurer  路  3Comments

MartynasZilinskas picture MartynasZilinskas  路  3Comments

CyrusNajmabadi picture CyrusNajmabadi  路  3Comments