Home > Net >  Why can’t a mapped type fully resolve a lookup using a generic, but finite, key?
Why can’t a mapped type fully resolve a lookup using a generic, but finite, key?

Time:10-31

Here’s a reduction of the problem:

type Animal = 'dog' | 'cat';

type AnimalSound<T extends Animal> = T extends 'dog'
    ? 'woof'
    : T extends 'cat'
    ? 'meow'
    : never;

const animalSoundMap: {[K in Animal]: AnimalSound<K>} = {
    dog: 'woof',
    cat: 'meow',
};

const lookupSound = <T extends Animal>(animal: T): AnimalSound<T> => {
    const sound = animalSoundMap[animal];
    return sound; 
}

Playground link

The return line is an error; the error message suggests that the sound variable is resolved to 'woof' | 'meow', even though it seems like TS should be able to type it as AnimalSound<T> based on the type of animalSoundMap. So why doesn’t the typechecker like it?

CodePudding user response:

In order to make TypeScript happy, I believe you should stick with @T.J. Crowder's solution or with this one:

type Animal = 'dog' | 'cat';

type AnimalSound<T extends Animal> = T extends 'dog'
    ? 'woof'
    : T extends 'cat'
    ? 'meow'
    : never;

const animalSoundMap: { [K in Animal]: AnimalSound<K> } = {
    dog: 'woof',
    cat: 'meow',
};

const lookupSound = <
    AnimalName extends Animal,
    AnimalMap extends { [Name in AnimalName]: AnimalSound<Name> }
>(animalMap: AnimalMap, animal: AnimalName):
    AnimalMap[AnimalName] =>
    animalMap[animal]

If you want to infer return type, you should also infer and make a part of function arguments animalMap. Playground

You don't even need to define explicit return type, TS is able to infer it from function body:

const lookupSound = <T extends Animal>(animal: T)=> {
    const sound = animalSoundMap[animal];

    return sound;
}

const result = lookupSound('cat') // "meow"

Conditional types does not work in a way you expect in a place of return type. It may work if you use conditional type in a function overloading:

function lookupSound<T extends Animal>(animal: T): AnimalSound<T>
function lookupSound<T extends Animal>(animal: T) {
    const sound = animalSoundMap[animal];
    return sound;
}


CodePudding user response:

I think the problem here is caused by AnimalSound<T> being a conditional type, and Typescript resolves conditional types later than other types; specifically, when T extends ... uses a type parameter, it is not resolved until T is bound to a concrete type. So inside the function, where T is just a formal type parameter, it can't reason about AnimalSound<T> in the way you want.

To avoid this, I recommend making animalSoundMap's type the one you use:

type AnimalSoundMap = {[K in Animal]: AnimalSound<K>}

const animalSoundMap: AnimalSoundMap = {
    dog: 'woof',
    cat: 'meow',
};

const lookupSound = <T extends Animal>(animal: T): AnimalSoundMap[T] => {
    return animalSoundMap[animal];
}

Playground Link

  • Related