THE PROBLEM: NO INFERENCE SITE FOR T
The problem you're having is that in the following call signature,
declare function getRandomName<T extends LivingBeing>(isPerson: T["isPerson"]): T["name"];
there isn't a good inference site for the generic type parameter T. You are hoping that the compiler will look at isPerson of type T["isPerson"], and use it to infer T. Unfortunately, it does not; indexed access types of the form T[K] cannot be used to infer either T or K.
There was at one point a pull request from one of the TS team members, microsoft/TypeScript#20126, which would have added support for this. Alas, it was never merged into the main branch and is not part of the language. It's not clear why from the issue history. Oh well.
Without an inference site for the generic type parameter T, and if you don't specify it manually like
getRandomName<Person>(true);
then inference will fail and the compiler falls back to its constraint:
getRandomName(true);
// function getRandomName<LivingBeing>(
// isPerson: boolean): "Peter" | "Robert" | "Jerry" | "Tom"
Since T was constrained to LivingBeing, it is inferred as LivingBeing, and LivingBeing["isPerson"] is just boolean, and LivingBeing["name"] is that union of four names. And you've lost any connection between true and false and the output type. Oops.
THE SOLUTION: PROVIDE AN INFERENCE SITE FOR T
If you want to get calls to getRandomName() to infer a type parameter properly, you'll need to give the compiler a nice easy inference site for it. The easiest and most straightforward way to do this is to make the type parameter exactly the type of the passed-in function parameter. You are passing in isPerson, so let's make the type of isPerson be the type parameter which we'll call P. We will have to constrain P to the valid types for isPerson, which is LivingBeing["isPerson"] (also called boolean, but we'll leave it like this). So the call signature will look something like this:
declare function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): ???;
In place of ??? we need to calculate the desired output type. First, given P, which member of the Person union is appropriate? This is essentially discriminating a discriminated union at the type level. We can use a conditional type to do that: specifically, the Extract<T, U> utility type:
// this is just to demonstrate; we don't need to use the type alias
type DiscriminateLivingBeing<P extends LivingBeing["isPerson"]> =
Extract<LivingBeing, { isPerson: P }>;
type TrueType = DiscriminateLivingBeing<true> // Person
type FalseType = DiscriminateLivingBeing<false> // Animal
And once we have that we just need to look up the name property. So here's the new call signature:
declare function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];
And we can test it:
const person = getRandomName(true)
// const person: "Peter" | "Robert"
Looks good. We're done, right? Uh, not quite:
A WRINKLE: GENERIC CONDITIONAL CALL SIGNATURES ARE HARD TO IMPLEMENT
If you try to implement getRandomName() exactly as before though, you'll run into an issue:
function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"] {
const names: Extract<LivingBeing, { isPerson: P }>["name"][] = // error!
isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random() * names.length)]; // error!
}
Inside the implementation of the function, P is an unresolved or unspecified generic type parameter. And the type Extract<LivingBeing, {isPerson: P}>["name"][] is a conditional type that depends on it. And when a conditional type depends on an unresolved type parameter, the compiler essentially gives up trying to understand if any particular value is assignable to it. It defers evaluating it entirely. So while a human being could go through the different possibilities for P and convince herself that isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"] is assignable to Extract<LivingBeing, {isPerson: P}>["name"][], the compiler is unable to do this. So it complains.
There is a feature request at microsoft/TypeScript#33912 which asks for some better support for implementing functions whose call signatures involve generic conditional types. For now though, you have to work around it with something like a type assertion to silence the compiler warning.
Usually in cases like this I make the function an overload with a single call signature. Overload function statement implementations are checked more loosely, so I make the call signature the fully correct one:
// call signature
function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];
And then the function implementation input and output types are widened enough to make the implementation work:
// implementation
function getRandomName(isPerson: LivingBeing["isPerson"]): LivingBeing["name"] {
const names = isPerson ? ["Peter", "Robert"] as const : ["Jerry", "Tom"] as const
return names[Math.floor(Math.random() * names.length)];
}
I used const assertions so the compiler would keep track of the string literal types of the strings in the array. But otherwise it is a simple implementation. If you really want you could loosen it even more and dispense with even that:
// implementation
function getRandomName(isPerson: boolean): string {
const names = isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random() * names.length)];
}
Either way, there's now no error, and you can call the function the way you want:
const person = getRandomName(true)
// const person: "Peter" | "Robert"
const animal = getRandomName(false)
// const animal: "Jerry" | "Tom"
Playground link to code