Take note that the object {a: 1, d: 4}
falls into the category of the Rec
type. In TypeScript, object types typically permit additional properties and do not adhere strictly to being "exact" as specified in microsoft/TypeScript#12936. This behavior is reasoned by factors relating to subtyping and assignability. For instance:
class Foo {a: string = "";}
class Bar extends Foo {b: number = 123;}
console.log(new Bar() instanceof Foo); // true
It's important to acknowledge that every Bar
includes a Foo
, hence it is not possible to assert that "all Foo
objects solely possess an a
property" without impeding class or interface inheritance and extension. Moreover, since interface
functions similarly, and given that TypeScript's typing system is inherently structural rather than nominal, there is no need to explicitly declare a Bar
type for its existence:
interface Foo2 {a: string};
// interface Bar2 extends Foo2 {b: number};
const bar2 = {a: "", b: 123 };
const foo2: Foo2 = bar2; // valid
Hence, whether positive or negative, we are confined within a type system where surplus properties do not interfere with type compatibility.
Naturally, this feature can lead to errors. Therefore, when assigning a brand new object literal to a specific object type, there exist comprehensive property checks that act as though the type were exact. These checks only activate under certain circumstances, such as in your initial example:
let rec: Rec = { a: 1, d: 4 }; // notification about excess property
However, return values from functions presently do not fall under these conditions. The type of the return value broadens before any superfluous property checks take place. An age-old unresolved GitHub issue, microsoft/TypeScript#241, suggests altering this behavior so that return values from functions refrain from expanding in this manner, although an attempt at rectification was initiated at microsoft/TypeScript#40311, ultimately discontinued, possibly never integrating into the language.
No perfect methods exist to repress surplus properties universally. My recommendation is to accept that objects might contain additional keys and validate that any code you compose remains intact regardless of this scenario. You can implement measures that dissuade surplus properties, like these:
// expressly specify return type
const fn2: Func = (): Rec => ({ a: 1, d: 4 }) // notification of excess property
// apply a generic type sensitive to extra properties
const asFunc = <T extends Rec & Record<Exclude<keyof T, keyof Rec>, never>>(
cb: () => T
): Func => cb;
const fn3 = asFunc(() => ({ a: 1, d: 4 })); // mistake! number is not supposed to be 'never'
Nevertheless, these approaches are intricate and prone to breaking, as nothing prevents you entirely from executing this action despite your efforts to safeguard your Func
type:
const someBadFunc = () => ({ a: 1, d: 4 });
const cannotPreventThis: Rec = someBadFunc();
Conceiving code that anticipates additional properties usually involves maintaining an array of recognized keys. Consequently, abstain from doing this:
function extraKeysBad(rec: Rec) {
for (const k in rec) {
const v = rec[k as keyof Rec];
console.log(k + ": " + v?.toFixed(2))
}
}
const extraKeys = {a: 1, b: 2, d: "four"};
extraKeysBad(extraKeys); // a: 1.00, b: 2.00, RUNTIME ERROR! v.toFixed not a function
Instead, opt for:
function extraKeysOkay(rec: Rec) {
for (const k of ["a", "b", "c"] as const) {
const v = rec[k];
console.log(k + ": " + v?.toFixed(2))
}
}
extraKeysOkay(extraKeys); // a: 1.00, b: 2.00, c: undefined
Access the Playground code link