james2mid
james2mid

Reputation: 49

Unusual TypeScript behaviour

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

Answers (2)

jcalz
jcalz

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!

Link to code

Upvotes: 0

Evert
Evert

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

Related Questions