Kerrick Kerrick - 2 months ago 24
TypeScript Question

TypeScript compiler thinks a recursive union type with a type index is a string?

Link to TypeScript Playground

interface Model {
[key: string]: string | number | boolean | Model | Model[];
}
interface ApiResponse {
[key: string]: Model[];
}
async function main(): Promise<void> {
const r = await api('foos_and_bars');
console.log(r.bars[0].baz.name); // Problem
console.log(r.foos[0].fuzzies[1].name); // Implicit any
}
main();
async function api(endpoint: string): Promise<ApiResponse> {
// Faked response:
return {
foos: [ { name: 'Foo 1', fuzzies: [{ name: 'Fuzzy 1' }, { name: 'Fuzzy 2'}] } ],
bars: [ { name: 'Bar 1', baz: { name: 'Baz 1' } } ]
}
}


In the TypeScript code above, I've got a
Model
interface that I intend to function like this:


  • Any model is an object with any number of properties

  • Those properties can be strings, numbers, booleans, another model, or an array of models.



But if I use this interface and try to read some properties on it, I get compiler errors I would not have expected.

For the first console log, I get:

Property 'name' does not exist on type 'string | number | boolean | Model | Model[]'.
Property 'name' does not exist on type 'string'.


For the second console log (with the
noImplicitAny
flag enabled):

Element implicitly has an 'any' type because type 'string | number | boolean | Model | Model[]' has no index signature.


What have I done wrong with my interface here?

Answer Source

The compiler is correct.

The first error message for:

console.log(r.bars[0].baz.name);

Says:

Property 'name' does not exist on type 'string | number | boolean | Model | Model[]'.
Property 'name' does not exist on type 'string'.

Which is exactly the case. The compiler doesn't know that r.bars[0].baz is of type Model, it knows that it can be of that type, but it can also be a string (or a number, etc).
Union types only have properties that all of the included types share.
You can inform the compiler that you know what the type is:

console.log((r.bars[0].baz as Model).name);

The same error is pretty much the same, the type of r.foos[0].fuzzies is not known, it can be one of the types in the union, so you need to let the compiler know in the same way:

console.log((r.foos[0].fuzzies as Model[])[1].name);