How to type check spread operator in Typescript?

Viewed 153

Is there a way to make this not compile? (because it does...)

const bar: { a: string } = {
    a: '',
    ...{b: ''}
}
1 Answers

I was shocked to find that this passed the type checker!

After poking around a bit, I have a strong hunch about why this is occurring, but I haven't found any official word to back it up yet. As such, this isn't an answer, but a conversation-starter that's too long for a comment. That's all to say, take what follows with a grain of salt.

I suspect that TypeScript's "ascription"/"annotation" mechanism allows for some wiggle-room based on subtypes. Specifically, when we write:

const foo: Foo = bar;

as long as bar's type is a subtype of Foo, this seems to pass the checker with flying colors. Here's a specific example:

class Foo {
  public a: string;
}

// `Bar` is a subtype of `Foo`
class Bar extends Foo {
  public b: string;
}

const foo: Foo = new Bar();
//         ^^^       ^^^ This is just fine!

Furthermore, since TypeScript's notion of subtyping is "structural", the following is also perfectly acceptable:

class Foo {
  a: string;
}

// `Quux` is structurally the same as (and therefore a subtype of) `Foo`
class Quux {
  a: string;
}

const foo: Foo = new Quux();

With this [theory] in mind, your example is accepted by the checker because { a: string; b: string } is a subtype of { a: string }.

In conclusion, if this is correct, I don't think there is a way to coax the type checker into rejecting your example.

Edit: This isn't the complete story. As @apflieger points out, this theory doesn't explain why

const foo: { a: string } = {
  a: 'test',
  b: 'test',
};

is rejected. It appears as though the spreading is critical here.

It could be that the type of

{
  a: 'test',
  ...{ b: 'test' }
}

is inferred as something like { a: string } & { b: string }, whereas in the previous case it is inferred as { a: string; b: string }.

Related