Skip to content

Overriding fields of a generic object using spread operator results in incorrect typesΒ #62831

Closed as duplicate

Description

@wnadurski

πŸ”Ž Search Terms

spread generic never typescript

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about spread

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.9.3#code/C4TwDgpgBAyglgWzAGzgMxAHgCoD4oC8UA3gNoDSEI240cAdlANZUD2aU2AugFycVUakLgF8oAMhIiA3AChZoSFABirVoRKyoUAIZ96AVwQAjCACc52432JbtUYwEY+AZ2BmGAc0tQZsv7IAxqz0blAIOmCq6kQ4UBAAHsAQ9AAmLipquAAUaGp82ACUhPjZttoAdFV5rAA0dnpQAORuHvSeTfVWNnZWAEx8fV2+9SKFckEhYTUaEVFqZbp8jrUONg7Oza1eTb6+45OhwBuu7l4aNRXGV45QAPR3UJfXTlCprBAu9E3HiXBu8mCoVYyAgFWQrE82SchXkimgNSE0CI8CQqAwmHh7CeWTkD3sUAAegB+IA

πŸ’» Code

type Simplify<T> = {[KeyType in keyof T]: T[KeyType]} & {};

type Foo = {
  a: number;
  b: {
    b1: string;
  };
};

const mapFoo = <T extends Foo>(foo: T) => ({
  ...foo,
  a: 'string',
  b: {
    b2: 2,
  },
});

const foo = mapFoo({ a: 1, b: { b1: 'string' } });
const b1: string = foo.b.b1 // foo.b.b1 doesn't exist

console.log(b1)

type fooType = Simplify<typeof foo>;
//    ^?

πŸ™ Actual behavior

The inferred type of the function mapFoo is:

const mapFoo: <T extends Foo>(foo: T) => T & {
    a: string;
    b: {
        b2: number;
    };
}

This is vastly different than what's actually in the runtime. A field resulting in type number & string and therefore never, is especially dangerous, since now it can be assigned to any variable:

const date: Date = foo.a // doesn't report any error

πŸ™‚ Expected behavior

The inferred type of the function mapFoo should be:

const mapFoo: <T extends Foo>(foo: T) => Omit<T, 'a' | 'b'> & {
    a: string;
    b: {
        b2: number;
    };
}

Additional information about the issue

Related issue: #50185 <- it was closed due to being a duplicate of #10727 , but I don't think they're duplicate. A type-level spread operator could help here, but the inferred type could be corrected using existing operators and types (Omit and &)

This issue doesn't happen for non-generic types, ie:

const mapFoo = (foo: Foo) => ({
  ...foo,
  a: 'string',
  b: {
    b2: 2,
  },
});

the inferred type is:

const mapFoo: (foo: Foo) => {
    a: string;
    b: {
        b2: number;
    };
}

Activity

  1. jcalz commented on Dec 3, 2025

    @jcalz
    Contributor

    Note: not a TS team member, feel free to ignore me

    This is indeed a duplicate of #50185 and #42690 and ultimately #10727. It's a duplicate because that's where they want to track all requests to accurately represent spread expressions in the face of possible property overwrites. If they decide to implement it using Omit, that would be a fix for #10727.

  2. wnadurski commented on Dec 3, 2025

    @wnadurski
    Author

    Thanks for the detailed explanation! I’d like to add a bit more context about why I raised this.

    First, I think this behavior is potentially a significant risk in real-world TypeScript codebases. In my case, it silently compromised type safety and spread through the code without being immediately obvious. Because the incorrect type inference appears valid, it can go unnoticed for a long time, which makes it especially concerning.

    Regarding #10727 - it looks like that proposal has been on hold for quite a while. I understand that introducing new syntax or operators is a large thing and probably not the most urgent priority. From my perspective, this issue doesn’t need a brand-new operator to be resolved quickly.

    I’m not familiar with the internals of TypeScript, but it seems like fixing this through the Omit type might be a relatively easy fix. I realize that such a change could break existing code, but in this case I think that’s appropriate: the current behavior hides mistakes rather than reporting them.

    Just wanted to clarify my reasoning

  3. jcalz commented on Dec 3, 2025

    @jcalz
    Contributor

    If it can be done with Omit then that implies you can do it yourself in userland, with the proviso that you need to replace spreads with some custom helper function that uses Omit. Indeed, such things have been done before, like in the SO question Merge object types?:

    type Foo = {
      a: number;
      b: {
        b1: string;
      };
    };
    
    const mapFoo = <T extends Foo>(foo: T) => (spread(foo, { a: 'string', b: { b2: 2, } }));
    
    const foo = mapFoo({ a: 1, b: { b1: 'string' } });
    //    ^? const foo: { a: string; b: { b2: number; }; }
    helper details
    type OptionalPropertyNames<T> =
      { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T];
    
    type SpreadProperties<L, R, K extends keyof L & keyof R> =
      { [P in K]: L[P] | Exclude<R[P], undefined> };
    
    type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never
    
    type SpreadTwo<L, R> = Id<
      & Pick<L, Exclude<keyof L, keyof R>>
      & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
      & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
      & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
    >;
    
    type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ?
      SpreadTwo<L, Spread<R>> : unknown
    
    function spread<A extends object[]>(...a: [...A]) {
      return Object.assign({}, ...a) as Spread<A>;
    }

    Playground link

    Any appetite to have this be part of the language itself as the return type of spread expressions will almost certainly be discussed in #10727. I'm pretty sure the reason it hasn't been implemented is summarized in #10727 (comment)

    We never found the balance of correctness and usability for spread types that justified their complexity. At the time, React higher-order components also required negated types in order to be correctly typed. Finally, we decided that since we'd been using intersection as a workaround for so long, that it was good enough.

  4. wnadurski commented on Dec 4, 2025

    @wnadurski
    Author

    I understand the reasoning, but I’d like to ask that this issue remain open, even if the long-term plan is to address it through a new operator in the future.

    Keeping this issue open makes it clear that this is a recognized bug in TypeScript, and it greatly helps with communicating the problem to other developers. Consider a realistic scenario: someone on a team uses the spread operator together with a generic object type and overrides some fields. In a PR review, you point out that this is unsafe due to a known TypeScript bug and ask them to use an omit helper function to ensure type-safety. It’s much easier to reference an open issue that clearly describes the problem and the workaround.

    If this issue is closed in favor of #10727 , it becomes much harder to provide a relevant explanation, since #10727 isn’t specifically about this behavior. Having this issue remain open until the actual root cause is fixed would make the situation clearer for anyone encountering it.

  5. jcalz commented on Dec 4, 2025

    @jcalz
    Contributor

    #42690 is open πŸ€·β€β™‚οΈ

  6. wnadurski commented on Dec 4, 2025

    @wnadurski
    Author

    Oh, true then I agree we can close this issue

  7. locked as resolved and limited conversation to collaborators on Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions