More TypeScript quirks: double standards for propagating type information

You might suspect that for type ObjType = {v: ..., x: ...} the two variables const v1: ObjType = {v: ..., x: ...}; and const v2 = {...v1, x: v1.x}; are guaranteed to be of the same type. Not so in TypeScript.

Matthias Falk

--

Consider this code fragment (Playground):

type Liberal = { v: string, x: 0 | 1 };
type Restrictive = { v: string, x: 1 };
type LiberalEnhanced = { v: string, u: number, x: 0 | 1 };
type RestrictiveEnhanced = { v: string, u: boolean, x: 1 };
type Merged = { v: string, u: number | boolean, x: 0 | 1 };
function processLiberal(o: Liberal) {};
function processRestrictive(o: Restrictive) {};
function distinguish(o: Merged) {
if (o.x === 1) {
processRestrictive(o); // type error here
}
};

When calling processRestrictive(o) within function distinguish, then TS considers the type of property x of o to be of type 0 | 1. The evaluation result of the condition o.x === 1 has not triggered a type narrowing. OK, that’s not nice but as expected. Assume that we do not want to narrow the type of o down to Restrictive to match the expectations of function processRestrictive since we do not want to cut off property u which we might need further below. So what to do? We could narrow just property o.x with an assertion function (Playground):

type Liberal = { v: string, x: 0 | 1 };
type Restrictive = { v: string, x: 1 };
type LiberalEnhanced = { v: string, u: number, x: 0 | 1 };
type RestrictiveEnhanced = { v: string, u: boolean, x: 1 };
type Merged = { v: string, u: number | boolean, x: 0 | 1 };
function processLiberal(o: Liberal) {};
function processRestrictive(o: Restrictive) {};
function assertsIsOne(x: number): asserts x is 1 {}; // <--
function distinguish(o: Merged) {
if (o.x === 1) {
assertsIsOne(o.x); // <--
processRestrictive(o); // type error here
}
};

Damn! Still the same type error, TS ignored my type assertion somehow. But what about this dirty trick (Playground):

type Liberal = { v: string, x: 0 | 1 };
type Restrictive = { v: string, x: 1 };
type LiberalEnhanced = { v: string, u: number, x: 0 | 1 };
type RestrictiveEnhanced = { v: string, u: boolean, x: 1 };
type Merged = { v: string, u: number | boolean, x: 0 | 1 };
function processLiberal(o: Liberal) {};
function processRestrictive(o: Restrictive) {};
function distinguish(o: Merged) {
if (o.x === 1) {
processRestrictive({...o, x: o.x}); // no type error
}
};

To my mind TS’s type propagation mechanism is not yet mature, to say the least and enforces you to spoil your code with such ugly workarounds. The problem seems related (if not the same) to this bug report which just happened to come up a few hours ago.

--

--