Typescript generics, constraints and literal types

Viewed 202

I have the following generic function:

type Identity = <T extends string | number>(arg: T) => T;
const identity: Identity = (x) => x;

If I attempt to use this function like so, I get errors that I cannot reassign.

let a = identity('a');
a = 'b'; // Error: Type '"b"' is not assignable to type '"a"'

let b = identity(1);
b = 2; // Error: Type '2' is not assignable to type '1'

I would expect the type returned from identity to be either a string or number, however it seems I am getting literal types back of 'a' and 1. Why is this the case? and is there a way to change my constraints so this is the behaviour?

I've have found a couple of workarounds. Firstly to assign the parameter to a variable first:

let a_param = 'a'
let a = identity(a_param);
a = 'b'; // Yay

let b_param = 1;
let b = identity(b_param);
b = 2; // Yay

And secondly to cast to a string or number:

let a = identity('a' as string);
a = 'b'; // Yay

let b = identity(1 as number);
b = 2; // Yay

Both of these feel wrong. I'm still learning TS so I think I've missed something.

2 Answers

The type parameter will normally be inferred as the most specific possible type consistent with the argument; in this case, the string literal type 'a'. This is almost always the desired behaviour.

In your case, the inferred type is too strict because you are using it as the inferred type of a variable, not just the inferred type of an expression. So a natural solution is to specify a weaker type parameter, like so:

let a = identity<string>('a');

However, it would be more normal to simply specify the type of the variable:

let a: string = identity('a');

You're using a generic but your requirements are antithetical to generics. This is because identity('a') actually means identity<'a'>('a') therefore the return type is 'a' (not any string).

What works (for some reason) is:

type Identity = {
  (arg: number): number;
  (arg: string): string;
}

const identity: Identity = (x: any) => x;

let a = identity('a');
a = 'b'; // yes
a = 3; // no

let b = identity(1);
b = 'b'; // no
b = 3; // yes

I think how this works is it resolves which of the overloads best matches within the type Identity when you call the function which implements that type.

Playground link

Related