11

I was wondering if there is a compiler option or something similar to make spreading objects strict.

Please see following example to understand what I mean:

interface Foo {
  a: string;
}

interface Bar {
  a: string;
  b: number;
}

const barObject: Bar = { a: "a string", b: 1 };

// should give a warning because Bar has more properties (here b) than Foo
const fooObject: Foo = { ...barObject };

// actually prints 1
console.log((fooObject as any).b); 

Is something like this possible?

ysfaran
  • 5,189
  • 3
  • 21
  • 51
  • Hmm, seems a bit inconsistent, since `const fooObject: Foo = {a: "hello"}; fooObject.b = 1;` *does* throw a compiler error, however spreading doesn't. – VLAZ Dec 13 '19 at 08:37
  • 1
    Ya, in fact so does `const foo: Foo = { ...bar, b: 2 }` would complain. No idea why. – Kousha Dec 13 '19 at 08:42
  • Also `const foo: Foo = Object.assign({}, { ...barObject }, {somethingElse: true})` does not complain... – Kousha Dec 13 '19 at 08:43
  • @Kousha moreover, the compiler *knows* the structure of `barObject`, as it correctly marks the spreading as safe, because the `a` property matches. If you do `const fooObject: Foo = { ...({x: 1, y: 2}) };` then it throws an error because there is no `a` property. It just never flags the extra properties in these cases. – VLAZ Dec 13 '19 at 08:45
  • 1
    OK, found some more questiosn abut this: https://stackoverflow.com/questions/48788052/using-spread-operator-in-typescript/ https://stackoverflow.com/questions/44525777/typescript-type-not-working-with-spread-operator/ https://stackoverflow.com/questions/47789057/typescript-return-type-is-not-fully-respected-and-can-include-unknown-keys/ basically, it seems like it's a long standing thing. Either live with it (after all, you the compiler will stop you from using `fooObject.b`) or write some helper function that only picks relevant properties to assign. – VLAZ Dec 13 '19 at 08:55
  • @VLAZ `fooObject.b` fails and `(fooObject as any).b` works *either way*, that's not inconsistent; having extra properties isn't a problem, accessing them through the wrong interface is. – jonrsharpe Dec 13 '19 at 08:59
  • @Kousha because that's an extra *literal* prop, think of it like runtime vs compile time. – jonrsharpe Dec 13 '19 at 09:01
  • @jonrsharpe that's still not okay; imagine I do a spread and then do POST that object to service; it will end up sending all of those extra parameters that I may not want to send. – Kousha Dec 13 '19 at 09:11
  • @Kousha something similar actually happened to me (while mapping frontend to backend models), which is why I'm asking this question. It's kind of cumbersome to find such an error. – ysfaran Dec 13 '19 at 09:14

1 Answers1

6

Interesting question. According to this issue, the result of the spread operator is intended to not trigger excess property checks.


// barObject has more props than Foo, but spread does not trigger excess property checks
const fooObject: Foo = { ...barObject };

// b is explicit property, so checks kicks in here
const foo: Foo = { ...bar, b: 2 }

There aren't exact types for TypeScript currently, but you can create a simple type check to enforce strict object spread:

// returns T, if T and U match, else never
type ExactType<T, U> = T extends U ? U extends T ? T : never : never

const bar: Bar = { a: "a string", b: 1 };
const foo: Foo = { a: "foofoo" }

const fooMergeError: Foo = { ...bar as ExactType<typeof bar, Foo> }; // error
const fooMergeOK: Foo = { ...foo as ExactType<typeof foo, Foo> }; // OK

With a helper function, we can reduce redundancy a bit:

const enforceExactType = <E>() => <T>(t: ExactType<T, E>) => t

const fooMergeError2: Foo = { ...enforceExactType<Foo>()(bar) }; // error

Code sample

ford04
  • 66,267
  • 20
  • 199
  • 171
  • 1
    so it's not really simple. I would rather have no checks instead of reducing readability of my code. Sad that there is no flag currently.. – ysfaran Dec 13 '19 at 11:49
  • In most cases, you probably *want* object spread to add additional properties. It wouldn't make so much sense to disable this feature across-the-board on a project-basis. I think, it would be more useful on a case basis, but here you could also enforce the type of the object before the spread operation (per variable or function parameter type), if you don't like readability of above code. You can also track the exact types issue (no idea, if it gets implemented). – ford04 Dec 13 '19 at 12:05
  • 3
    @ysfaran one addition: object spread currently behaves in accordance with the whole structural type system/duck typing, where a sub type can be assigned to a base type. In a normal variable assignment you only get excess property checks, when you initialize a fresh object literal. For example you can write `const fooObject: Foo = bar`, because `bar` isn't a fresh object literal anymore and a sub type of `Foo`. `const fooObject: Foo = { ...bar }` behaves analogously, `bar` is not "fresh" anymore/ has already been declared before. – ford04 Dec 13 '19 at 18:02