Object types in TypeScript are not "sealed"; excess properties are allowed. This enables all sorts of good stuff like interface and class extension, and is just part of TypeScript's structural type system. It is always possible for excess properties to sneak in, so you should probably make sure that your foo()
or bar()
implementations don't do anything terrible if the function argument has such properties. If you iterate over properties via something like the Object.keys()
method, you should not assume that the result will be just known keys. (This is why Object.keys(obj)
returns string[]
)
Of course, throwing away excess properties is often indicative of a problem. If you pass an object literal directly to a function, then any extra properties on that literal will most likely be completely ignored and forgotten about. That could be indicative of an error, and so object literals undergo checking for excess properties:
function foo(a: Options) {
return a.str;
}
const resultA = foo({
str: "a",
extraOption: "errors as expected", // error
});
The reason why this goes away for something like
function bar<T extends Options>(a: T) {
return a.str as T["str"];
}
const resultB = bar({
str: "a",
extraOption: "no error!?",
});
is because generic functions do have the possibility of keeping track of such extra properties:
function barKT<T extends Options>(a: T) {
return { ...a, andMore: "hello" }
}
const resultKT = barKT({ str: "a", num: Math.PI });
console.log(resultKT.num.toFixed(2)) // "3.14"
But in your version of bar()
you are only returning the str
property, so excess properties really will be lost. So you'd like an error on excess properties, and you're not getting one.;
But this raises the question: why is bar()
generic? If you don't want to allow excess properties and you only care about the one str
property, then there's no obvious motivation for T extends Options
. If you only want to possibly narrow the type of T["str"]
, then in fact you only want that part of the input to be generic:
function barC<S extends Options["str"]>(a: { str: S }) {
return a.str;
}
const resultC = barC({
str: "a",
extraOption: "error", // error here
});
But if you really do neet T extends Options
for some reason, you can discourage excess properties by making the function input be of a mapped type where excess properties have their property value types mapped to the never
type. Since there are no values of type never
, the object literal passed in cannot meet that requirement, and they will generate an error:
function barD<T extends Options>(a:
{ [K in keyof T]: K extends keyof Options ? T[K] : never }
) {
return a.str
}
const resultD = barD({
str: "a",
extraOption: "error", // error here
});
Hooray!
Again, while this discourages excess properties, it does not absolutely prevent them. Structural subtyping requires that you can widen a {str: "a" | "b", extraOption: string}
to {str: "a" | "b"}
:
const someValue = {
str: "a",
extraOption: "error",
} as const;
const someOptions: Options = someValue; // okay
barC(someOptions); // no error
barD(someOptions); // no error
So again, you should make sure your implementations don't assume that excess properties are impossible.
Playground link to code