Noé Rubinstein
Noé Rubinstein

Reputation: 910

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

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?

Upvotes: 3

Views: 93

Answers (2)

jcalz
jcalz

Reputation: 328262

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

Upvotes: 4

kschaer
kschaer

Reputation: 126

Take a look at what the compiler shows when you hover over N - In your generic function, when you write N extends Greeting["name"] you're really saying N extends "Hello" | "Hi!".

This means that the return value of bar is in fact of type { name: "Hello" | "Hi!" }, and not Greeting.

Though this is similar to the type you've declared for Greeting, these are not in fact equivalent; hence the type error you're encountering.

Upvotes: 0

Related Questions