TypeScript Playground
interface Something {
a: string;
}
interface Test {
[key: string]: Something | undefined
}
// Expecting: Something | {}
// Inferred: {}
const getSomething = (t: Test) => t['key'] || {}
// Usage:
declare const test: Test;
// Expecting: string | undefined
// Inferred: Error: `a` does not exist on type
const a = getSomething(test).a;
What is the reason that a union of {}
with an object erases the object's type?
If I write:
type EmptyObject = { [key: string]: undefined }
and use {} as EmptyObject
instead, then things work as expected.
Formally, union of {}
with any non-primitive (object) type is reduced to just {}
for the purpose of type checking. It's because in TypeScript, {}
as a type does not really mean "an object with no properties", it means "an object with no known properties", so that usual type compatibility rules can be applied and {}
can serve as ultimate super type.
So, adding anything to the union with {}
does not really add any information known about the type - nothing is still known about properties of resulting type.
But inferring {}
as the type for empty object literal in this code
const getSomething = (t: Test) => t['key'] || {}
indeed causes information loss, because value {}
is known to have no properties, and because of that, if the resulting type has any properties they must have come from Something
and a
must have type string
if it's present at all.
Maybe in the future TypeScript will be able to handle this with exact types, but for now, there's one possible workaround which you found.
If you use type assertion as {[n: string]: undefined}
for empty object,
you get the expected result because now the resulting type is calculated by going over common properties of the union type members and taking union of their types. So Test
has Something
as property type, and undefined
comes from type assertion, giving Something | undefined
as resulting property type.
Conventionally, never
type is used instead of undefined
, so you can define
type StricterEmptyObject = { [n in string]?: never };
which, again, works as expected because never
is the opposite of {}
and union of never | undefined | Something
is undefined | Something
.
// Inferred return type: Something | StricterEmptyObject
const getSomething = (t: Test) => t['key'] || {} as StricterEmptyObject;
declare const test: Test;
// inferred: string | undefined
const a = getSomething(test).a;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With