Why does TS Lose Type Inferences When Spreading

74 Views Asked by At

Update 1️⃣

As per comments, I did create a standalone example of what seems to be the same situation:

interface Person {
  name: string;
  age: number;
}

type People = Person[];

const p: Person = {
  name: "Mark",
  age: 39,
};

const people: People = [p];

const anotherP = {
  ...people[0],
  age: 40,
};

However...this doesn't create any issue!

So, now the issues might be from the actual types from: import type { Meta, StoryObj } from '@storybook/react';.

Anyway, as seems to be the way to go for this particular situation.


Consider:

const meta = {
  args: {
    items: [
      {
        isValid: null,
        stepNumber: '1',
        text: 'Customer Information',

        children: 'Content',
      },
    ],
  },

If I remove 1️⃣ of the s, I get the expected pushback from TS. This confirms that the types are all good in the provided snippet.

However, further along in the code I have:

export const Valid: StoryObj<typeof meta> = {
  args: {
    items: [
      {
        ...meta.args.items[0],
        isValid: true,
      },
    ],
  },
};

In this part specifically:

{
        ...meta.args.items[0],
        isValid: true,
      },

TS says, "Type { isValid: true; stepNumber?: string | undefined; text?: string | undefined; children?: string | undefined; } is not assignable to type CheckoutAccordionItemProps. Types of property stepNumber are incompatible. Type string | undefined is not assignable to type 'string'. Type undefined is not assignable to type string..."

For more visibility ️, here's this:

export interface CheckoutAccordionItemProps {
  isValid: boolean | null;
  stepNumber: string;
  text: string;

  children: ReactNode;
}

To summarize, even though:

args: {
    items: [
      {
        isValid: null,
        stepNumber: '1',
        text: 'Customer Information',

        children: 'Content',
      },
    ],
  },

is fine, the act of spreading: ...meta.args.items[0], seems to introduce undefined possibilities.

This can be resolved by using: ...(meta.args.items[0] as CheckoutAccordionItemProps),.

Why do we need to use this ugly type assertion as? Spreading should not have compromised the original type information.

1

There are 1 best solutions below

0
CodeFinity On

Whenever we spread an object with TypeScript, it doesn't just take the values and keys at face value. Instead, TypeScript tries to represent the types of the output object as accurately as possible, including the possibility that some keys might not exist on the source object even if they do at runtime, resulting in undefined being a 'possibility.'

{
    ...meta.args.items[0],
    isValid: true,
}

TypeScript interprets this as "Take all properties from meta.args.items[0] and add/overwrite the isValid property with the value true". But TypeScript doesn't guarantee that all properties of meta.args.items[0] exist (even if we know that they do). It adds the possibility of undefined to each of them.

{
    isValid: true;
    stepNumber?: string | undefined;
    text?: string | undefined;
    children?: string | undefined;
}

This is a safe behavior because in JavaScript, if you were to spread an object that didn't have one of the expected keys, the resulting object would indeed have that key set to undefined.

We're forced to use a type assertion to tell TypeScript, "Trust me, I know what I'm doing".

{
    ...(meta.args.items[0] as CheckoutAccordionItemProps),
    isValid: true,
}