typescript: validate excess keys on value, returned from function

Note that {a: 1, d: 4} is of the Rec type. Object types in TypeScript generally allow excess properties and are not “exact” as requested in microsoft/TypeScript#12936. There are good reasons for this having to do with subtyping and assignability. For example:

class Foo {a: string = ""}
class Bar extends Foo {b: number = 123}
console.log(new Bar() instanceof Foo); // true

Note that every Bar is a Foo, which means that you can’t say “all Foo objects only have an a property” without preventing class or interface inheritance and extension. And since interface works the same way, and since TypeScript’s type system is structural and not nominal, you don’t even have to declare a Bar type for it to exist:

interface Foo2 {a: string};
// interface Bar2 extends Foo2 {b: number};
const bar2 = {a: "", b: 123 };
const foo2: Foo2 = bar2; // okay

So for better or worse we are stuck with a type system whereby extra properties do not break type compatibility.


Of course, this can be a source of errors. So in the case where you are explicitly assigning a brand new object literal to a place that expects a particular object type, there are excess property checks that behave as if the type were exact. These checks only happen in particular circumstances, as in your first example:

let rec: Rec = { a: 1, d: 4 }; // excess property warning

But return values from functions are not currently one of these circumstances. The return value’s type gets widened before any excess property checks can happen. There is a quite old open GitHub issue, microsoft/TypeScript#241 which suggests that this should be changed so that return values from functions not be widened this way, and there’s even an implementation of a potential fix at microsoft/TypeScript#40311 but it was closed so it might never make it into the language.


There aren’t any perfect ways to suppress excess properties in general. My advice is to just accept that objects may have excess keys and ensure that any code you write would not break if this is the case. You can do things which discourage excess properties, such as these:

// annotate return type explicitly
const fn2: Func = (): Rec => ({ a: 1, d: 4 }) // excess property warning

// use a generic type that gets mad about excess properties
const asFunc = <T extends Rec & Record<Exclude<keyof T, keyof Rec>, never>>(
    cb: () => T
): Func => cb;
const fn3 = asFunc(() => ({ a: 1, d: 4 })); // error! number is not never

But they are more complicated and easily broken, since nothing whatsoever will stop you from doing this no matter how much you try to safeguard your Func type:

const someBadFunc = () => ({ a: 1, d: 4 });
const cannotPreventThis: Rec = someBadFunc();

Writing code that anticipates extra properties usually involves holding onto an array of known keys. So don’t do this:

function extraKeysBad(rec: Rec) {
    for (const k in rec) { 
        const v = rec[k as keyof Rec];  // bad assumption that k is 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

Do this instead:

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

Playground link to code

Leave a Comment