4

Why does the following code compile successfully? Since bar is not part of MyState, I would expect it to generate a compiler error.

type MyState = { foo: number; };
type Reducer<T> = (state: T) => T;

const wtf: Reducer<MyState> = (state) => {
  return { foo: 123, bar: 123 }; // `bar` isn't part of MyState
};
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Dan
  • 1,729
  • 1
  • 18
  • 25
  • Why would it. MyState only requires foo, if it has more it still fulfills the definition of MyState. – Erik Philips Aug 24 '17 at 18:05
  • 1
    @ErikPhilips The compiler will complain about this: `const myState: MyState = { foo: 123, bar: 123 };` and will say "Object literal may only specify known properties, and 'bar' does not exist in type 'MyState'." – Shaun Luttin Aug 24 '17 at 18:12

2 Answers2

6

crashmstr's answer is correct but it's worth explaining why this case is different from one where you do get an error.

Object literals only cause extra property errors in specific cases.

In this case:

var x: MyState = { foo: 10, bar: 20 };

The type system does the following steps:

  • Check that MyState is a valid type (it is)
  • Check that the initializer is valid:
    • What is the type of the initializer?
      • It is the fresh object type {foo: 10, bar: 20}
    • Is it assignable to MyState?
      • Is there a foo property?
        • Yes
      • Does its type match?
        • Yes (10 -> number)
      • Are there any extra properties from a fresh type?
        • Yes
          • Error

The key thing here is freshness. A type from an object literal is fresh if it comes directly from the object literal itself. This means there's a difference between

// Error
var x: MyState = { foo: 10, bar: 20 };

and

// OK
var x1 = { foo: 10, bar: 20 };
var x2: MyState = x1;

because the freshness of the object literal vanished once it was assigned into x1.

Your example suffers the same fate - the freshness of the object literal is gone once it became part of the function return type. This also explains why the error re-appears if you have a return type annotation on the function expression.

Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • aside from nominal typing, are there any plans on allowing for more sound restrictions for cases like these? – Dan Aug 24 '17 at 18:41
  • This isn't a soundness deficit. No incorrect type is observed; the function returns a proper subtype. – Ryan Cavanaugh Aug 24 '17 at 18:43
  • Sorry, I'm likely using the term "sound" naively. I'm just trying to prevent typos in my reducers, but within the current type system it's proving difficult. – Dan Aug 24 '17 at 18:46
4

Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing.

Type Compatibility

To check whether y can be assigned to x, the compiler checks each property of x to find a corresponding compatible property in y. In this case, y must have a member called name that is a string. It does, so the assignment is allowed. Note that y has an extra location property, but this does not create an error. Only members of the target type (Named in this case) are considered when checking for compatibility.

So in your example, { foo: 123, bar: 123 } meets the requirement of having a foo that is a number, and the extra bar is ignored for type compatibility.

Note: See also Why am I getting an error “Object literal may only specify known properties”?

crashmstr
  • 28,043
  • 9
  • 61
  • 79
  • 1
    Why does the compiler complain about the following then? `const wtf2 (state: MyState) : MyState => { return { foo: 123, bar: 123 };` It says, "Object literal may only specify known properties, and 'bar' does not exist in type 'MyState'." – Shaun Luttin Aug 24 '17 at 18:09
  • 1
    See also [Why am I getting an error “Object literal may only specify known properties”?](https://stackoverflow.com/questions/31816061/why-am-i-getting-an-error-object-literal-may-only-specify-known-properties) – crashmstr Aug 24 '17 at 18:19
  • @crashmstr I downvoted because the compiler complains when we use a non-generic form of the same function. The answer does not address why the generic form allows unknown properties whereas the non-generic form disallows unknown properties. – Shaun Luttin Aug 24 '17 at 18:23
  • Well, specifically there is a case where *Object Literals* are held to a higher standard and I don't know where in the documentation that is specified. That is also *not part of the question* as it currently stands, just a comment. – crashmstr Aug 24 '17 at 18:25
  • The question's function implementation returns an object literal, so object literals are part of the question as it currently stands. – Shaun Luttin Aug 24 '17 at 18:27
  • The function returns an object literal, but the function is not explicitly typed in the current question (it is assigned to a variable that defines the return type). It does get the error if you explicitly type the function's return. But it you assign the object literal to a variable first and then return that variable, there is no error. If you want to find where that mechanism is defined in the language specification, more power to you. – crashmstr Aug 24 '17 at 18:29
  • This happens because the freshness of the object literal is removed when it becomes the inferred return type of the function expression; generics are not at all related. See my other answer in this question. – Ryan Cavanaugh Aug 24 '17 at 18:37