Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type union not checking for excess properties

Tags:

typescript

let's imagine a have an object that either have properties A and B or C, e.g.:

const temp = {
  A: 1,
  B: 2,
}

or

const temp = {
  C: 3,
}

And intuitively I see this type as:

type T =  {A: number, B: number} | {C: number};

const valid: T = {A: 1, B: 2};
const alsoValid: T = {C: 3};

// Should complain but it does not
const invalid: T  = {A: 1, B: 2, C: 3};
// Also should complain
const alsoInvalid: T = {A:1, C: 3};

But TS treats such type as {A?: number, B?: number, C?: number} and basically | makes fields optional but I want TS to complain about inconsistent type when I add C property to A and B


How can I archive the desirable type?

like image 513
qwerty qwerty Avatar asked Jan 20 '21 08:01

qwerty qwerty


People also ask

How to solve the property does not exist on type Union error?

The "property does not exist on type union" error occurs when we try to access a property that is not present on every object in the union type. To solve the error, use a type guard to ensure the property exists on the object before accessing it. Here is an example of how the error occurs.

Does the type 'DESC' exist on type 'unionbox' PS?

Receiving next error: Property 'desc' does not exist on type 'UnionBox' ps. I understand that there must be some weird algorithm, Just not sure in which way to look See this answer. The case is very similar. When TypeScript resolves union type it allows you to use best common type. I mean the type which is common for all union elements.

What is the type of a union box?

UnionBox from your example can be one of the types - FruitBox, Vegabox, or IceBox. IceBox is the one that makes Typescript unhappy - it doesn't contain a property desc. Imagine having an object of type IceBox, then passing it to a function, which takes UnionBox as a parameter and then uses the property desc - it would access a non-existing one.


2 Answers

This is a bit of a quirk in how unions work in conjunction with excess property checks. {A:1, C: 3} is actually compatible with {C: number} except for excess property checks:

const o = {A:1, C: 3};
const ok: {C: number} = o; // No direct literal assignment, no excess property checks
const nok: {C: number} = { A:1, C: 3}; // Excess property checks kick in 

Playground Link

And the quirk of excess property checks is that for unions, it allows any property from any union constituent to be present in the assigned object literal.

You can get an error if the union constituents are incompatible one with another:

type T =  {A: number, B: number} | {C: number, A?: undefined, B?: undefined };

const valid: T = {A: 1, B: 2};
const alsoValid: T = {C: 3};

// Error
const invalid: T  = {A: 1, B: 2, C: 3};
//Error
const alsoInvalid: T = {A:1, C: 3};

Playground Link

You can also use the StrictUnion from here if the union has a lot of memebers


type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

type T =  StrictUnion<{A: number, B: number} | {C: number }>;

const valid: T = {A: 1, B: 2};
const alsoValid: T = {C: 3};

// Error
const invalid: T  = {A: 1, B: 2, C: 3};
//Error
const alsoInvalid: T = {A:1, C: 3};

Playground Link

like image 199
Titian Cernicova-Dragomir Avatar answered Oct 17 '22 02:10

Titian Cernicova-Dragomir


I just always add some type of union. For example:

type T = { A: number, B: number, type: 'ONE' } | { C: number, type: 'TWO' };


const valid: T = { A: 1, B: 2, type: 'ONE' };
const alsoValid: T = { C: 3, type: 'TWO' };

// Should complain but it does not
const invalid: T = { A: 1, B: 2, C: 3, type: "ONE" };
// Also should complain
const alsoInvalid: T = { A: 1, C: 3, type: 'TWO' };

The goal is to have some common property

like image 21
captain-yossarian Avatar answered Oct 17 '22 02:10

captain-yossarian