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