Elaborating Ryan’s ideas a bit
As a finger exercise to get more proficient with TypeScript I elaborated Ryan’s ideas (see TypeScript Playground). My implementation contains two new ideas. The first one is a simplification of Ryan’s Subtract
(see my first response to Ryan’s article), the second is the observation that Ryan overlooked the possibility of unions: For example, Ryan’s IsWhole<3 | 5>
evaluates to true
while it (probably) should evaluate to false
.
My core idea to distinguish unions of natural numbers from single natural numbers (a special case of unions) is the following: After having checked with Ryan’s methods that a union contains only natural numbers, find the minimum of the union and Exclude
it. If the union now is empty (type never
), then we know that the minimum we just had excluded was the only member of the union, so this union was in fact a single natural number.
How do we find the minimum (of a union of natural numbers)? We check inductively whether a natural number (type) I
is element of the union, i.e. we start with I=0
, then (recursively) with I=1
and so on until we find the first match (which is guaranteed since we deal with a union of natural numbers). Elementhood just means extends
, i.e. I extends Union ? …
means for the true branch just that I
∊ Union
.
I have based my Multiply implementation on direct recursion, rather than using an accumulator construction as Ryan did, but this is just a matter of taste.
Arithmetic on the type level: Just a gimmick or does it have serious applications?
My intention to look for such an implementation was that I missed a certain (generic) convenience type, a type that allows tuples of a certain type with a finite range of elements. For example, UnionOfImmutableTuplesWithLengthRange<2, 4, string>
should evaluate to [string, string] | [string, string, string] | [string, string, string, string]
. Imagine the second number would be 17
, rather than 4
, then such a type IMO is a considerable improvement over the literal, spelled out one.
A related convenience type that I actually use is RangeOfNumbers
, with e.g. RangeOfNumbers<3, 12>
⟶ 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
.
Lessons learned: Some disappointment about (the current) TypeScript
I am a newbie to TypeScript and my initial enthusiasm has cooled down a bit in the meantime. Yes, I had heared before that TypeScript is unsound by design but I considered the typical examples for illustrating these limitations rather harmless: Problems can arise with typecasts and any
. That’s already all what I had noticed so far on this topic.
During this finger exercise I experienced another kind of unsoundness, that is IMO unrelated to the above described kind of unsoundness (Ryan has already mentioned it). It is a consequence of TypeScript’s limited recursion/nesting depth. I do not critisize this limitation by itself but the fact that type evaluations (for conditional types) stop at some (rather arbitrary) recursion/nesting depth and uses the intermediate result as if it were the valid final result from a proper recursive evaluation. Not throwing any kind of error or warning in such a situation (i.e. aborting recursion due to a reached depth limit) is IMO an embarrassing behavior. A consequence is, e.g., that my NonFailsafeMultiply<7, 8>
evaluates to 16
, while NonFailsafeMultiply<4, 5>
evaluates correctly to 20
.
I admit that doing arithmetic with TypeScript (on the type level) is an abuse of this language. However, I can imagine that taking an intermediate result in (recursive) type evaluation silently as the final result can lead to frustrating bugs in more serious applications. I have submitted a bug report but Ryan Cavanaugh’s immediate reaction just minutes after I had published my report IMO gives not rise to much hope that Microsoft is willing to change this behavior. If you share my opinion that TypeScript should throw some error or warning if it aborts type evaluation due to reached depth limits, it might help if you supported my request as a comment on this bug report.