types-spring icon indicating copy to clipboard operation
types-spring copied to clipboard

Why not linked optional type?

Open Sanshain opened this issue 1 year ago • 2 comments

Why not linked optional type?

type LinkedOptions = LinkedOptionalType<{a: number, b: number, c: number}>
const a: LinkedOptions   // {a?: number} | {a: number, b?: number} | {a: number, b: number, c?: number}

At first glance, it looks convenient and cool: such a type would allow very quickly prototyping types in which the presence of some fields depends on others. But this is only at first glance. Consider the following example:

let a: {a?: string} | {a: string, b?: string} | {a: string, b: string, c?: string} = {
	a: '',
	c: ''
}

First of all, the Union used to narrow down the type to a single field a. This is the only field that you can use without type guard. For all other (optional) fields, you will need type guarding. And the only type guarding that allows you to do this is the in operator. But in this case it is not safe:

if ('c' in a){
	a.b.toString()   // ts is ok, but runtime error
}

From the typescript point of view, everything is ok here, but if you run it, then in fact you will get a runtime error. This defect can be eliminated by setting the suppressExcessPropertyErrors option to false. However, by default, this option is set to true. So such a unique type would not work by default

An alternative solution could be this:

let a: {a?: string, b: undefined, c: undefined} | {a: string, b: string, c: undefined} | {a: string, b: string, c: string} = {
	a: '',
	c: ''
}

if (typeof a.b == 'string'){
	a.b.toString()
}

Then typescript will say directly that variable a lacks field b and it cannot be deceived, as in the example above.

But using this type is even less convenient, and the universal type implementation is very complicated to this. Also I can definitely say that its implementation is impossible with such a signature:

type LinkedOptions = LinkedOptionalType<{a: number, b: number, c: number}>

since, according to the standard, it is impossible to regulate the sequence of keys in an object. If you have a strong desire, you can implement such behavior using a conditional type. It is more difficult to write, but is type-safe:

type T<A extends string|undefined, B extends string|undefined> = {
	a: A, 
	b: A extends string ? B : never,
	c: A extends string 
		? B extends string ? string : never
		: never
}

unfortunately, creating such a universal type is very difficult to implement and I consider it impractical. Therefore, my advice is to avoid such cases or use conditional types for manual prototyping.

Sanshain avatar Jun 27 '23 07:06 Sanshain