Why is a TypeScript record assignable to a partial object with clearly different types?

Viewed 298

Consider the following program. Playground.

const x: Record<string, string> = { foo: "bar" };
const y: { baz?: number | undefined } = x; // TypeScript doesn't complain

In the above example, I would have expected TypeScript to complain that string is not assignable to number | undefined. However, the above code type checks. Why?

1 Answers

Typescript is a structurally typed language, ie if two types have a similar structure they are assignable to each other.

// Example 1 - with no Excess property check (Because of implicit narrow type Record<string, string>)

const x: Record<string, string> = { foo: "bar" };

const y: { baz?: number } = x

// Example 2 - with Excess property check

const y1: { baz?: number } = { foo: 'bar' } // Error


// Also to prove Record<string,string> is assignable to { [k: string]: any }

type Assignable = Record<string, string> extends { [k: string]: any  } ? true: false // true

Code Plyaground

Here when you do x: Record<string, string> it basically means x: { [k: string]: string } which is wider type than the actual { foo: "bar" }

And for explicitly annotated types, Excess property checks are not done, meaning we can supply additional props to it along with the required prop that is baz.

But since baz is partial and not required any other excess props can be added to it and baz will be inferred as undefined. hence typescript doesn't complain

To prove my point we can check that Record<string, string> extends { baz? :number } ? true: false will be true

However if we directly try to assign an object literal type of the same Record type, typescript will complain as now the excess property check does happen.

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error.

Side Note:

Record<string, string> is not the preferred type for this scenario. The idiomatic way of doing that is to use an index signature like { [k: string]: string }

Because given x: Record<string, string>, x.foo will apparently be a string at compile-time, but in actuality is likely to be string | undefined. This is a gap in how --strictNullChecks works

Related