Yavanosta
Yavanosta

Reputation: 1670

Typescript generic and NonNullable

I have a typing issue with my generic function. The issue appears though several third-party libraries, but I managed to build minimal example.

interface MyType {
  id: string;
}

function myFunction<T extends MyType>(param: T) {
  // Type 'string' is not assignable to type 'NonNullable<T["id"]>'.
  const a: NonNullable<T["id"]> = "my-id";
  console.log(a);
}

If I rewrite this function without generic, typing works fine:

function myFunction(param: MyType) {
  const a: NonNullable<MyType["id"]> = "my-id";
  console.log(a);
}

I checked generics documentation and can't find why the first code is incorrect. Can someone help me with that?

Upvotes: 4

Views: 708

Answers (1)

jcalz
jcalz

Reputation: 328548

Up to and including TypeScript 4.7, the NonNullable<T> utility type was implemented as a conditional type, T extends null | undefined ? never : T.

When a conditional type depends on a generic type parameter, as in the type NonNullable<T["id"]> inside the body of myFunction(), the compiler completely defers evaluation of it; it just doesn't know what type it is yet, at all. Such types are essentially opaque to the compiler, so it cannot verify that a value of any other type is assignable to it. You need to refactor or use type assertions to perform such an assignment:

const a = "my-id" as NonNullable<T["id"]>; // okay, TS4.7-

Luckily, TypeScript 4.8 will introduce some better intersection reduction and narrowing support that includes re-defining NonNullable<T> as T & {}. That is, instead of a conditional type, it's just an intersection with the empty object type (which can be anything except null or undefined). Details about how this works are in microsoft/TypeScript#49119, but for your purposes all you need to know is that your original example will just start working:

const a: NonNullable<T["id"]> = "my-id"; // okay TS4.8+

Playground link to code

Upvotes: 1

Related Questions