Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Union with empty object results in type erasure

Tags:

typescript

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.

like image 717
Shayan Avatar asked Mar 06 '19 21:03

Shayan


1 Answers

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;
like image 166
artem Avatar answered Oct 13 '22 21:10

artem