Reputation: 49
Say I have the following TypeScript code:
type FieldArray = string[]
type FieldSet = Record<string, any>
type FieldParams =
| [ FieldArray, FieldSet ]
| [ FieldArray ]
| [ FieldSet ]
function convertFieldParams (fields: FieldParams): Field[] {
const normalised = typeof fields[1] === 'undefined'
? fields[0] instanceof Array
? [ fields[0], {} ]
: [ [], fields[0] ]
: fields
// ...
}
I would expect the normalised
variable to have a type of [ FieldArray, FieldSet ]
due to the type checking, although it's showing in my editor as Record<string, any>[]
.
As I understand it, TypeScript has some understanding of the possible code paths and should be able to calculate the type here.
Am I misunderstanding or is this a bug in TS?
Upvotes: 0
Views: 95
Reputation: 328196
I'd say there may be a bug here in narrowing down a union of tuples via control flow analysis when you check the contents of one of its elements (i.e., typeof fields[1] === "undefined"
should eliminate one of the legs of that union, but it's not happening). I found an issue reporting the same or a similar thing, but it doesn't look like anyone's spent much time looking at it. If you care you might want to go to that issue and give it a 👍 or present a new example of where it fails.
However, in your case, I'd change your test to take advantage of the fact that tuples in TypeScript have fixed lengths known at compile time. If you check fields.length === 1
, the narrowing you expected with typeof fields[1] === "undefined"
will happen automatically.
The other issue here is that when you create an array literal like const arr = [1, 2, 3]
, TypeScript will tend to infer an array type like number[]
instead of a tuple type like [number, number, number]
or even a tuple of literals like [1, 2, 3]
. If you want some other inference, you need to give it a context that hints at such an inference. (You can read about the rules the compiler uses to infer narrower/wider types here.) For example, an annotation like const arr: [number, number, number] = [1, 2, 3]
will serve to give the compiler the opportunity to check the type instead of inferring it. If you write const arr: [number, number, number] = [1, 2]
it would be an error. In your case, if you expect normalised
to be of type [FieldArray, FieldSet]
, you should annotate it as such and the compiler will warn you if you've gotten that wrong.
Here's the changed code:
function convertFieldParams(fields: FieldParams): void {
const normalised: [FieldArray, FieldSet] =
fields.length === 1
? fields[0] instanceof Array
? [fields[0], {}]
: [[], fields[0]]
: fields;
}
That works without errors now. Hope that helps; good luck!
Upvotes: 0
Reputation: 99533
You are understanding it correctly, and it's probably not a bug.
This sentence is 100% correct:
As I understand it, TypeScript has some understanding of the possible code paths and should be able to calculate the type here.
The keyword here is "some". Typescript can be pretty good at inferring types, and will do its best to do so, but it's imperfect. What Typescript can and cannot automatically infer tends to grow every release, but I wouldn't go as far as calling it a bug. At most it's a feature that's not developed yet.
Upvotes: 1