typescriptdiscriminated-union

Typescript not allowing a union type in an array


I'm having trouble understanding discriminated unions in typescript in the context of an array. Using the example from the documentation, I would expect the below to be perfectly valid.

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;

const getShapes = (): Shape[] => {
    const data = [
        { kind: "circle", radius: 1 },
        { kind: "square", sideLength: 3}
    ]
    return data;
}

playground link

My understanding is this is saying "a Shape must be either a Circle or a Square".

Instead it gives this error:

Type '({ kind: string; radius: number; sideLength?: undefined; } | { kind: string; sideLength: number; radius?: undefined; })[]' is not assignable to type 'Shape[]'. Type '{ kind: string; radius: number; sideLength?: undefined; } | { kind: string; sideLength: number; radius?: undefined; }' is not assignable to type 'Shape'. Type '{ kind: string; radius: number; sideLength?: undefined; }' is not assignable to type 'Shape'. Type '{ kind: string; radius: number; sideLength?: undefined; }' is not assignable to type 'Square'. Types of property 'kind' are incompatible. Type 'string' is not assignable to type '"square"'.

I'm specifically confused about seeing kind: string as how typescript interpreted that, when it matches the literal. Based on how I'm reading the docs, this seems like a perfectly valid use for a union


Solution

  • When presented with a variable declaration const data = ⋯ with no type annotation, TypeScript infers the type of the variable from the initializer, based on heuristic inference rules that work well in a wide range of scenarios. It does not currently have the ability to defer that and "look ahead" to see how the variable is used later. Given

    const data = [
      { kind: "circle", radius: 1 },
      { kind: "square", sideLength: 3 }
    ];
    

    the compiler infers

    const data: ({
        kind: string;
        radius: number;
        sideLength?: undefined;
    } | {
        kind: string;
        sideLength: number;
        radius?: undefined;
    })[]
    

    because that's what inference rules say to do. In particular, string literal properties get widened to string, since it's very common for people to modify the values of properties. The compiler does not see that you intend to return data as Shape[], so it doesn't know that you intend for the kind property to have string literal types. By the time you return data is encountered, it is too late. The type of data is already set.


    If you want to fix this you should either annotate data like const data: Shape[] = ⋯, or you could use the satisfies operator on the initializer to give the compiler the context it is missing:

    const getShapes = (): Shape[] => {
      const data = [
        { kind: "circle", radius: 1 },
        { kind: "square", sideLength: 3 }
      ] satisfies Shape[]
      return data;
    }
    

    Playground link to code