TypeScript: Unexpected unknown

Viewed 362

I've been working with TypeScript quite a bit lately and stumbled across a problem that is confusing to me. I've put the problem into an example which looks like the following:

type Options<T> = {
  props: T;
  g: (x: T) => void;
};

function f<T>(options: Options<T>) {
  console.log(options);
}

// Example 1 (x and props expected type)
f({
  props: {
    foo: {
      a: 200,
      bar: () => {}
    }
  },
  g(x) {
    // x is the expected type
    console.log(x)
  }
});

// Example 2 (x and props unknown)
f({
  props: {
    foo: {
      a: 100,
      bar: function() {}
    }
  },
  g(x) {
    // x is unknown
    console.log(x)
  }
});

When hovering over props in the first example, you will see that it has the expected type. And although barely anything changed in the second example props is of type unknown. Why is that? Here is the example above in TS Playground.

2 Answers

This is a design limitation in TypeScript; see microsoft/TypeScript#38845. Apparently (from this comment):

An arrow function with no parameters is not context sensitive, but a function expression with no parameters is context sensitive because of the implicit this parameter. Anything that is context sensitive is excluded from the first phase of type inference, which is the phase that determines the types we'll use for contextually typed parameters.

It looks like inference for T succeeds when the foo.bar property is an arrow function because there's no contextual this to worry about. And so a contextual type is successfully assigned to the x parameter of g, and everything proceeds as you want. But when the value of foo.bar is a function expression, the compiler needs context to figure out what this is, and T cannot be inferred in time to figure out a contextual type for g's parameter and it ends up becoming unknown.

As @jcalz mentioned this is because typescript cannot figure out in what kind of context the expression is. Here type-inference you can read more about that in contextual typing. One way to fix your code should explicit give the type any to g: (x: any) => void and then type your function for example.

interface Options<T> {
  props: T;
  g: (x: any) => void;
};


f({
  props: {
    foo: {
      a: 100,
      bar: function() {}
    }
  },
  g(x: string) {
    // x is string
    console.log(x)
  }
});

But I don't know if this is best practice.

Related