Skip to content

Suggestion: perform excess property checks when spreading an inline object literal #39998

Open

Description

TypeScript Version: 3.9.2

Search Terms:

Code

declare const someCondition: boolean;

type MyObject = { foo: number; bar?: number };

const a: MyObject = {
  foo: 1,
  bar: 2,
  // ✅ Error because `invalid` is an excess property
  invalid: 3,
};

const b: MyObject = {
  foo: 1,
  ...(someCondition
    ? {
        bar: 2,
        // ❌ `invalid` is an excess property, but we don't get an error here
        invalid: 3,
      }
    : {}),
};

In the example above, I only want to include specific properties when a condition is met. That's the only reason I'm using spread here.

I understand that TypeScript only performs excess property checks inside of object literals. Currently this does not include inline object literals which are being spread inside of another object literal.

In my experience this is a very common code pattern so it would be great if TypeScript handled this.

Expected behavior:

An error

Actual behavior:

No error

Playground Link:

Related Issues:

Activity

  1. ifeltsweet commented on Aug 21, 2020

    @ifeltsweet

    Absolutely agree with the suggestion here. We do stuff like this all the time:

    const company: Partial<Company> = {
      ...(update.name ? { name: update.name } : null),
      ...(update.email ? { email: update.email } : null),
      ...(update.alias ? { aliassssss: update.alias } : null), // no error
    };
  2. OliverJAsh commented on Feb 22, 2021

    @OliverJAsh
    ContributorAuthor

    Just realised, this issue isn't just about excess property checks. We're also missing errors for non-excess properties (#41698):

    declare const someCondition: boolean;
    
    type MyObject = { foo: number } & ({ bar: number } | {});
    
    const a: MyObject = {
        foo: 1,
        // ✅ Error
        bar: 'foo',
    };
    
    const b: MyObject = {
        foo: 1,
        ...(someCondition
            ? {
                  // ❌ Expected error, got none
                  bar: 'foo',
              }
            : {}),
    };
  3. maksimf commented on Apr 24, 2021

    @maksimf

    Is there any update on this? This is really annoying that ts doesn't check that

  4. dwjohnston commented on Aug 9, 2021

    @dwjohnston

    Note that this issue isn't just on spreading immediately declared object literals, spreading a function parameter can introduce this issue:

    type Bar = {
        a: string; 
    };
    
    type Chaz = {
        a: string, 
        c: "zip" | "zap"
    }
    
    function usesBar(value: Bar, c: "zip" | "zap") : Chaz {
        return {
            c, 
            ...value, // The excess properties will clobber the c value!
        }; 
    }
    
    const d = {
        a: "hello", 
        c: "blah blah blah"
    }; 
    
    const e = usesBar(d, "zip"); 
    console.log(e);
    // {
    //  "c": "blah blah blah",
    //  "a": "hello"
    // }
  5. gerasimvol commented on Apr 12, 2022

    @gerasimvol

    same bug

    type Params = {
      a?: any;
      b?: any;
    }
    
    function func (params: Params) {
      return;
    }
    
    const objWithAllowedKeys = { a: 2 }
    const objWithForbiddenKeys = { c: 2 }
    
    func({ ...objWithAllowedKeys }) // valid - no error ✅
    func({ ...objWithForbiddenKeys }) // valid - error ✅
    func({ ...objWithAllowedKeys, ...objWithForbiddenKeys }) // invalid - why no error? ❌
  6. jamesa3 commented on May 24, 2022

    @jamesa3

    Any update on this?

  7. bdpartridge commented on Nov 23, 2022

    @bdpartridge

    We've been caught by surprise a few times by this in production; specifically in cases where we renamed an optional property as part of some refactor. To be fair, unit tests could've caught the issues too; we were lacking some coverage. But this behavior can be a little surprising, even for those that have been using TypeScript for a while. Probably because it feels inconsistent with how excess property checking works for object literals in general.

  8. RCnowak commented on Jan 23, 2023

    @RCnowak

    Any update on this? In 2023 we rely too much on a tool which has such a big hole in type security. I can use 'strict' flag but I still loose types due to spreading my objects.

  9. juanrgm commented on Feb 24, 2023

    @juanrgm

    Same case here and very surprised 😕.

    https://www.typescriptlang.org/play?#code/MYewdgzgLgBCBGArApsKBGAXDA3gWACgZiZkwBDeAG2QBNt4QQbyxCBfGAXl0JNIrU62KACcArsgA0fEgEswAN3JU59GGMlSYAeh2lRokKNnEAdBfxF+JVgE8oACwUBzORGWr1m5Lv1gQAyMTa2J2DkJCUEhYBBQ0ACZuXlCBShpvCWlTGAVPNREs7T0g4xyLMysbWzAHZzA3DxUCjSy-GADSkP5wgk4Icih3ADM5ZAgU-jJ04RhGZmRWDiA

    const object1: {
        enabled: boolean
    } = {
        enabled: true,
        invalid: true, // error
        ...{
            anythingisvalid: true // no error
        }
    }
    
    const object2 = {
        enabled: true,
        invalid: true, // error
        ...{
            anythingisvalid: true // no error
        }
    } satisfies {
        enabled: boolean
    }
  10. thepuzzlemaster commented on Mar 3, 2023

    @thepuzzlemaster

    I came across this unexpectedly as well when trying to conditionally add some properties to a return object.
    https://stackoverflow.com/questions/75621849/why-does-using-spread-synax-not-honor-type-safety-for-an-expected-return-ty/75622021#75622021

  11. Kasik-D commented on May 14, 2023

    @Kasik-D

    I wrote type to check is T type compare to K type, but I don't know how this code improve and transform to use on function

    type Test = { k: string, n : number }
    
    const test = { n: "", k: '' }
    
    
    type isCurrentObject<T extends object, K extends object> = keyof T[]['length'] extends keyof K[]['length'] ? keyof T extends keyof K ? true : false : false
    
    
    type a = isCurrentObject<Test, typeof test> 

    Playground

  12. Harpush commented on Jun 7, 2023

    @Harpush

    A very much needed fix... Spreading objects is a really common case

  13. devmargooo commented on Jun 15, 2023

    @devmargooo

    Same bug even in very simple case

    type Foo = {
        foo: string;
    }
    
    type Bar = {
        bar: string;
    }
    
    const bar:Bar = { bar: "" };
    const foo:Foo = { ...bar, foo: "" }; // ❌ invalid but no error 
    

    https://www.typescriptlang.org/play?ssl=10&ssc=37&pln=1&pc=1#code/C4TwDgpgBAYg9nKBeKBvAUFLUBmCBcUAzsAE4CWAdgOYDc6AvuuqJFAEICGpyam2AI26ESFGvSboAxnEokoQ0vi48UqBcKgAiLVAb0Zc4LgLxEaqADprigDQm4hHXtpA

  14. megaboich commented on Mar 28, 2024

    @megaboich

    I recently encountered this issue and was surprised to find that TypeScript ignores the error. It's astonishing to see that this problem has been around since at least 2017 without being resolved. It's quite disappointing.

  15. vtgn commented on May 14, 2024

    @vtgn

    Hi!
    I'm totally disappointed too because of this illogical behavior. I've got the problem with a simple case too, similar to the one of devmargooo above.
    It is sad that Typescript can't detect this problem. We have to wait the execution of the software and the class-validator validation of the object to detect the error, whereas TypeScript could have done it as early as during coding in the IDE and during the build. :'(

  16. dpeter99 commented on Aug 26, 2025

    @dpeter99

    I have just run into this at work, while trying to figure out whay I wasn't getting "may only specify known properties" warnings.

  17. zacaj commented on Jan 9, 2026

    @zacaj

    If TS can't validate satisfies when a spread is used, at the very least it should error and not allow spreads to be used- this is silently ignoring the whole point of using satisfies as opposed to other type assertions

  18. OliverJAsh commented on Feb 18, 2026

    @OliverJAsh
    ContributorAuthor

    It's worth mentioning the need for spreading arises when using exactOptionalPropertyTypes.

    Without exactOptionalPropertyTypes, you don't need to use spread (ref):

    type MyObject = {
      a: string;
      optional?: string;
    };
    
    declare const condition: boolean;
    
    const obj1: MyObject = {
      a: 'hello',
      optional: condition ? 'world' : undefined,
    };

    With exactOptionalPropertyTypes, you need to use spread (ref):

    type MyObject = {
      a: string;
      optional?: string;
    };
    
    declare const condition: boolean;
    
    // ❌ Error
    const obj1: MyObject = {
      a: 'hello',
      optional: condition ? 'world' : undefined,
    };
    
    const obj2: MyObject = {
      a: 'hello',
      ...(condition && { optional: 'world' }),
    };
  19. aweebit commented on Aug 21, 2026

    @aweebit

    This would be really, really useful for React / JSX as simplifying code like this

    <MyComponent
      myPropA={myPropA}
      myPropB={myPropB}
      myPropC={myPropC}
      myPropD={myPropD}
      myPropE={myPropE}
    />

    by deduplicating prop / variable names like this

    <MyComponent {...{ myPropA, myPropB, myPropC, myPropD, myPropE }} />

    would no longer mean that no excess property checking would be done.

    A currently available workaround is to add a satisfies annotation:

    <MyComponent
        {...({
            myPropA,
            myPropB,
            myPropC,
            myPropD,
            myPropE
        } satisfies React.ComponentProps<typeof MyComponent>)}
    />;

    But it's very clumsy and makes the code confusing for no reason.

  20. aweebit commented on Aug 21, 2026

    @aweebit

    The problem with conditionally added properties also affects JSX:

    declare const MyComponent: (props: { required: string, optional?: string }) => React.JSX.Element;
    declare const condition: boolean;
    <MyComponent required="example" {...(condition ? { OPTIONAL: 'example' } : null)} />

    An error should obviously be reported here.

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

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions