Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic function allow arbitrary keys

Tags:

typescript

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
};
like image 509
Dan Avatar asked Jan 04 '23 13:01

Dan


2 Answers

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.

like image 186
Ryan Cavanaugh Avatar answered Feb 26 '23 09:02

Ryan Cavanaugh


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”?

like image 30
crashmstr Avatar answered Feb 26 '23 08:02

crashmstr