Home > Mobile >  Why does a branded string/number type made DeepWriteable no longer extend string/number?
Why does a branded string/number type made DeepWriteable no longer extend string/number?

Time:01-24

I have a DeepWriteable type and a branded number type PositiveNumber. PositiveNumber extends number, but DeepWriteable<PositiveNumber> does not. Same story for a branded string type. What's going on here?

type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
type PositiveNumber = number & { __brand: 'PositiveNumber' };
type NonEmptyString = string & { __brand: 'NonEmptyString' };

type test1 = PositiveNumber extends number ? true : false;
// TRUE
type test2 = DeepWriteable<PositiveNumber> extends number ? true : false;
// FALSE
type test3 = NonEmptyString extends string ? true : false;
// TRUE
type test4 = DeepWriteable<NonEmptyString> extends string ? true : false;
// FALSE

(BTW, I'm only asking about branded types because regular number and string don't seem to have this behavior)

type test5 = DeepWriteable<number> extends number ? true : false;
// TRUE
type test6 = DeepWriteable<string> extends string ? true : false;
// TRUE

I've resorted to just adding a check to DeepWriteable to not map over numbers and strings which works fine, but I'm curious why it's necessary.

Is it because the -readonly transformation makes this a non-homomorphic mapped type? I think I can maybe see why this happens for string, because length is readonly. Is there a similar readonly property on number?

CodePudding user response:

Any mapped type (even homomorphic ones) on a branded primitive (a primitive type which has been intersected with an object-like type) will end up destroying the primitive type.

Homomorphic mapped types on regular, non-branded, primitives were special-cased in microsoft/TypeScript#12447 to return the primitive without alteration.

But for branded primitives you get the mapping over all the properties and apparent properties of the input type, which means DeepWriteable<PositiveNumber> will give you a result with all the Number interface properties as well as a __brand property.

As mentioned in this comment on the related issue microsoft/TypeScript#35992:

Branding a primitive with an object type is an at-your-own-risk endeavor. Basically you can do this as long as the type never goes through a simplifying transform ... That said, I'm describing the current behavior, not outlining any promises of future behavior. T... We do use some branding internally but don't file bugs against ourselves when we encounter weirdness

  • Related