3

I am a little confused about the situation that I have encountered while working with interfaces in TypeScript. I have a function that accepts an object in a particular shape.

const add = (val: {n1: number, n2: number}): number => {
    return val.n1 + val.n2;
};

If I send an object that does not exactly match the shape (an addition key for example) by storing it in a variable, the compiler does not complain and compiles successfully.

Compiler accepts this:

let params = {n1: 100, n2: 200, foo: "x"};
add(params);

But doesn't accept this:

let params = {n1: 100, n2: 200, foo: "x"};
add({n1: 100, n2: 200, foo: "x"}); 
//Object literal may only specify known properties.

As mentioned in TypeScript documentation, the only thing that matters is the shape not the additional keys. The thing that I wonder is why the first one is accepted?

amone
  • 3,712
  • 10
  • 36
  • 53

2 Answers2

7

Typescript does not have a concept of exact types.

Object types in typescript do not mean that all instances of that type have ONLY those properties. Basic inheritance allows us to create a sub types of a given type with the sub type having more properties. Consider:

interface I {n1: number, n2: number}
interface D extends I { foo: string }

The principles of object oriented programming also dictate that we should be able to assign a sub-type wherever a base type is expected.

Given these rules, it is not surprising that this works:

let params = {n1: 100, n2: 200, foo: "x"};
add(params);

Since the typescript type system is structural, it relies less on nominal inheritance and more on the shape of the types. The type of params has the structure to make it a sub-type of {n1: number, n2: number} and thus the assignment is allowed.

The odd rule (at least as far as the principles outlined above) is the excess property rules for object literals. This ignores the assignability of a sub-type to the base-type in favor of catching a certain class of errors. But this excess property check only applies to when an object literal is directly assigned to a reference of a specific type. If you assign indirectly, the type of params is inferred from the object literal and we fall back to the sub-type is assignable to base-type rule.

While typescript does not have the concept of strict types, we can mimic this effect for function parameters with a generic function and mapped and conditional types:

let params = {n1: 100, n2: 200, foo: "x"};

interface IAddParms {n1: number, n2: number }
function add<T extends IAddParms>(p: T & Record<Exclude<keyof T, keyof IAddParms>, undefined>) {
    return p.n1 + p.n2
}
add({n1: 100, n2: 200, foo: "x"});  // error
add(params) // error
add({n1: 100, n2: 200 }) // ok
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
1

There are certain things in typescript which trigger "Excess Property Check" and things that don't, this is one of those cases.

Typescript at it's heart is structurally typed meaning the above code is unlikely to actually cause errors because as long as its a object with those two keys it doesn't matter how many excess keys it has this is a trade-off the typescript team made.

You can read about it here in more depth: https://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks

And turn on excess property check permanently like this if you should so desire. Forcing excess-property checking on variable passed to TypeScript function

Hope this helps let me know.

Shanon Jackson
  • 5,873
  • 1
  • 19
  • 39