Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: why does this trivial generic function not behave like its non-generic equivalent?

Tags:

typescript

type Greeting = { name: "Hello" } | { name: "Hi!" };

export function foo(name_of_greeting: Greeting["name"]): Greeting {
  return { name: name_of_greeting };
}

export function bar<N extends Greeting["name"]>(name_of_greeting: N): Greeting {
  return { name: name_of_greeting };
}

foo typechecks fine, but bar produces the following error:

Type '{ name: N; }' is not assignable to type 'Greeting'.

Naively, I would expect bar to be completely equivalent to foo. Why does TypeScript disagree?

like image 254
Noé Rubinstein Avatar asked Jan 27 '26 01:01

Noé Rubinstein


1 Answers

The TypeScript compiler does not in general break object types with union-typed properties into unions of object types with non-union properties. Before TypeScript 3.5 this was not done at all (both functions fail in TS3.3). TypeScript 3.5 introduced "smarter" union type checking in which some concrete unions of properties are sometimes checked this way... and your foo() function compiles successfully.

Theoretically one could always break objects-of-unions into unions-of-objects: for example, {a: string|number; b: boolean; c: object | null} could be exploded into {a: string, b: true, c: object} | {a: string, b: true, c: null} | {a: string, b: false, c: object} | {a: string, b: false, c: null} | {a: number, b: true, c: object} | {a: number, b: true, c: null} | {a: number, b: false, c: object} | {a: number, b: false, c: null}). But, this would likely be a huge performance penalty and would only be beneficial in some use cases.

I assume it would be even more difficult/expensive to manipulate union-typed properties when the type is generic, where {a: T} where T extends 0 | 1 would be some generic type U extends {a: never} | {a: 0} | {a: 1}. So the language hasn't done it. This could be considered either intentional or a design limitation of TypeScript.


Anyway, my workaround here would be to widen the returned value in bar() to a concrete type that the compiler will correctly check, like this:

export function bar<N extends Greeting["name"]>(name_of_greeting: N): Greeting {
  const concreteGreeting: { name: "Hello" | "Hi!" } = {
    name: name_of_greeting
  };
  return concreteGreeting;
}

Okay, hope that helps; good luck!

Link to code

like image 51
jcalz Avatar answered Jan 31 '26 17:01

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!