2

I have a readonly Pokemon[] array named PokemonData. Each element has a name member. Is it possible to make a type that is a concatenated list of pokemon names?

interface Pokemon {
    readonly name: string;
    // ...
}

const PokemonData: readonly Pokemon[] = [{
    name: 'Pikachu',
    // ...
}, {
    name: 'Bulbasaur',
    // ...
}] as const;

type PokemonName = // TODO: should be 'Pikachu' | 'Bulbasaur'

I could easily do this if I had an array of just the names:

const pokemonNames = [ 'Pikachu', 'Bulbasaur' ] as const;
type ArrayType<T> = T extends readonly (infer U)[] ? U : never;
type PokemonName = ArrayType<typeof pokemonNames>;

I suspect, if this is possible at all, I need the object-property equivalent of my ArrayType<T> above and/or a function that takes in a Pokemon and returns its name.

// using a type approach (doesn't work)
type GetPokemonName<T extends Pokemon> = T['name'];
type GetPokemonNames<T> = T extends Pokemon[] ? GetPokemonName<T[/* something here */]> : never;

// using a function approach (may work, but I don't know how to convert it to a type)
function getPokemonName(pkmn: Pokemon) {
    return pkmn.name;
}

I also thought maybe the ReadonlyArray<T>.map() would be useful, but it just returns a string type.

const pokemonNames = PokemonData.map(p => p.name); // using 'as const' throws a compile-time error
type PokemonName = ArrayType<typeof pokemonNames>;

1 Answer 1

2

The only way Typescript will be able to see the names in the array as string literal types is if you remove the : readonly Pokemon[] type annotation; otherwise any typeof is going to be looking at the declaration of the Pokemon type rather than a more specific type inferred from the array's actual contents.

If you remove that annotation, then the names can be mapped to a union type:

const PokemonData = [{
    name: 'Pikachu'
}, {
    name: 'Bulbasaur'
}] as const;

type PokemonName = (typeof PokemonData)[number]['name'];

The [number] maps the array type to its component type, and then ['name'] maps to a union of the string literal types associated with the name properties.

This isn't an ideal solution because removing the Pokemon[] type annotation means Typescript won't check that its contents are valid Pokemon objects. If you want that, you can write a helper function:

function pokemon<K extends string>(obj: Pokemon & { name: K }): Pokemon & { name: K } {
    return obj;
}

const PokemonData = [
    pokemon({
        name: 'Pikachu',
    }),
    pokemon({
        name: 'Bulbasaur',
    }),
] as const;

Playground Link

Sign up to request clarification or add additional context in comments.

1 Comment

A late addition: From TS4.9 you can use the satisfies keyword to declare an inferred literal but still force it to be assignable to a type, in this case const PokemonData = [[name: 'Pikachu'], {name: 'Bulbasaur'}] as const satisfies Pokemon[];

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.