Typescript: Infer based on generics

Viewed 162

When placing a type guard for arg1 I'd expect arg2 to be inferred automatically, but it doesn't.

Anyone an idea why this isn't working? what's the best alternative? Thanks so much!

You can see the types in action here. Simply hover over arg2 to see the inferred type.

function example<T extends 'foo' | 'bar'>(
  arg1: T, 
  arg2: T extends 'foo' ? string : number
) {
  if (arg1 === 'foo') {
    arg2 // should be string?
  }

  if (arg1 === 'bar') {
    arg2 // should be number?
  }
}

However, invoking the function does correctly apply the second arg type:

enter image description here

enter image description here

Here's another example using a typed object based on generics:

type Test <T extends boolean> = {
  multiple: T;
  value: T extends true ? string : number;
};

// this doesn't work?
function test <T extends boolean>(a: Test<T>) {
  if (a.multiple) {
    a.value // string | number?
  }else{
    a.value // string | number?
  }
}

// this works!
function test2 (a: Test<true> | Test<false>) {
  if (a.multiple) {
    a.value // is string
  }else{
    a.value // is number
  }
}

See playground

2 Answers

I use these 2 solutions to have conditional arguments:

Solution 1 (recommended): Use object for arguments

type Args = { arg1: 'foo'; arg2: string } | { arg1: 'bar'; arg2: number };
function example(args: Args): void {
  if (args.arg1 === 'foo') {
    args.arg2; // It's string
  }
  if (args.arg1 === 'bar') {
    args.arg2; // It's number
  }
}

Solution 2: Use arguments object

type Args = ['foo', string] | ['bar', number];
function example(...args: Args): void {
  if (args[0] === 'foo') {
    args[1]; // It's string
  }
  if (args[0] === 'bar') {
    args[1]; // It's number
  }
}

Please consider this snippet:

const k = 'foo' as 'foo' | 'bar'

example(k, 'abc') // compiles ok
example(k, 1) // compiles ok

arg2 is not tightly coupled with arg1. Still when you call example with a literal string: 'foo' or 'bar' as arg1 Typescript can restrict type of arg1 and infer exact type of arg2. But in general case this is not possible.

As a possible fix you could restrict args of example, like that:

function example2(...args: ['foo', string] | ['bar', number]) {
  if (args[0] === 'foo') {
    const s = args[1] // string
  }

  if (args[0] === 'bar') {
    const n = args[1] // number
  }
}
example2('foo', '') // ok
example2('bar', 0) // ok
example('foo', 0) // error
example2(k, 'abc') // error
example2(k, 1) // error

Playground

Situation with type Test<T extends boolean> is very similar. The definition of type does not restrict strictly enough possible values. For example:

const v1: Test<boolean> = {
  multiple: true,
  value: 'a'
}

const v2: Test<boolean> = {
  multiple: true,
  value: 1
}

Using Test<true> | Test<false> makes use of literal type and restricts set of possible values.

Playground

Related