Home > Enterprise >  Infer type T using type assertion of other type which is conditioned on T
Infer type T using type assertion of other type which is conditioned on T

Time:10-08

Consider this function that retrieves either an "item" or an array of "items":

function foo<T extends Item | Item[]>(id: T extends Item ? string : undefined): T {
  ...
}

The purpose of this is that you call foo with an id if you want a single item, or without an id if you want an array of items. This works well so that the caller may do:

foo<MyItem>('42')

and

foo<MyItem[]>(undefined)

(One could make this even better using varags such as function foo<T extends Item | Item[]>(...[id]: T extends Item ? [string] : []): T { to enable calling foo without arguments when retrieving an array).

However, inside the implementation of foo, I noticed that if I put a check for the type of id:

if (id) { // if typeof id === 'string' gives same result
  ...
}

... then in that if clause, the compiler does not understand that the type T must now extend Item and not Item[] (due to id being a string only if T extends Item. I still need to do T as Item if I want to call any code that relies on T being an Item, or I will get a:

TS2344: Type 'T' does not satisfy the constraint 'Item'.   Type 'Item | Item[]' is not assignable to type 'Item'.     Property 'id' is missing in type 'Item[]' but required in type 'Item'.

In other words, the type of id is conditioned on the type of T, but the type of T is not inferred back/narrowed by asserting that id is a string. Is it possible to solve this somehow? It's an easy as cast, but I'm simply curious.

CodePudding user response:

In essence, you want a function to return different things depending on the passed arguments.

This is a job better suited for overloads.

type Item = {};

function foo<T extends Item>(): T[];
function foo<T extends Item>(id: string): T;
function foo<T extends Item>(id?: string): T | T[] {
  const item = {} as T; // for demo purpose only

  if (id){
    return item;
  }

  return [item];
}

const test1 = foo();      // Item[]
const test2 = foo('1');   // Item

Check the playground here.

As a general rule of thumb, use overloads if your typing logic involves function implementation. Conditional types are usually more useful when defining types or interfaces that don't deal with runtime logic.

CodePudding user response:

You can make the return type conditional on the argument type, instead of the other way around:

function foo<T extends string | undefined>(id: T): T extends string ? Item : Item[] {
  // ...
}

Or if you will only ever want to call it with something that is either definitely a string or definitely undefined, you can use overloads and not make it generic at all: this way you can call the function without an argument, instead of passing undefined as an argument.

function foo(): Item[];
function foo(id: string): Item;
function foo(id?: string): Item | Item[] {
  // ...
}

The practical difference between these solutions is that using overloads, your function can't be called with an argument of type string | undefined, so it's always known at compile-time whether a particular call will resolve to a single item or an array of items.

Note that both solutions avoid the problem of calling your function with a type parameter like Item & HTMLCanvasElement and then having an unsound return type.

  • Related