Warning of some surprising quirks and “design limitations” in TypeScript

If you use conditional types, be aware of some surprising limitations of TS’s inference capabilities, as e.g. encountering an undocumented magic number 25

Matthias Falk
6 min readSep 12, 2021

TypeScript (TS) is a syntactically rich language which allows a lot of interesting type constructions. However, having learned the lesson from painful debugging sessions in type programming, I discourage you to give in to the temptation to exhaust TS’s advanced type construction options. Its inference engine (e.g. for the evaluation of conditional types) might not keep up with the language’s syntactic possibilities.

For the first class of limitations TS is not to blame. They stem from the fact that TS is a statically, rather than a dynamically typed language. To make the difference clear, think of the following code fragment:

type Yes = ...
type No = ...
function f(p: Yes) { ... };
let v: Yes | No;
...
v = ... // set v's actual value by user input
f(v);

You have two types Yes and No and a function f that requires a parameter p of type Yes. You declare a variable v of type Yes | No and assign it the value of, say, a user input. Later in code you call f(v). No doubt, this is poor design, but that is not the point here. The compiler of a statically typed language, such as TS, cannot know at compile time whether the call of f(v) is admissible. Even if your program logic somehow guaranteed that v would be set to a value of type Yes, the compiler could not know and would flag this usage of f(v) as erroneous.

If you used a dynamically typed language instead and your program logic were correct, then your program would run through without a type error since the admissibility of v’s type would be checked at runtime, at the call time of f(v).

So, what you can type-check with TS is already limited by its nature of being statically typed. This is not the point I am complaining about here but the ignorance of information that in fact is available at compile time.

In the following I list two “design limitations” (Microsoft’s labeling in bug reports) and one behavior I want to call a quirk. All three issues came as a surprise to me and incited my suspicion that there will be more unpleasant surprises I do not know about yet.

Design limitation 1: The magic number 25 in the expansion of cross products

Conditions in conditional types are questions about subtyping:

type Condition<T, U> = T extends U ? V : W;

If the actual type T’ given for type parameter T is a subtype of the actual type U' given for type parameter U in the instantiation Condition<T',U'> of the generic type Condition, then Condition<T',U'> evaluates to type V, otherwise to type W. However, the evaluation result of the condition (the answer to the question whether T' is a subtype of U' or not) is sometimes wrong. I have a case where TS gives the answer No (meaning T’ does not extend U’), where the correct answer in fact is Yes. In my particular case it has to do with the expansion of the members (objects) of cross products. If the expansion has 25 or less members, then everything works correctly. If the cross product has more members, then TS stops the evaluation and answers the question with No.

Minor complaints that immediately came to my mind when learning about this issue were: a) Why did no documentation or text book tell me about this limitation?, b) Why is this “magic number” so ridiculously small? and c) Why is this magic number (if existing at all) not configurable?. However, my main complaint is that TS self-confidently gives the answer No where it really cannot be sure since it has stopped its computation prematurely. Why did it not just throw an error message or warning instead? I consider this behavior unacceptable, with other words: a bug. What do you think?

The issue reported here might seem very artificial and you might be tempted to shrug your shoulders: Expansion of cross products? I don’t care about such esoteric features. You might be right for this particular case but the deeper problem is that arbitrary “magic numbers” (if it only were 42 instead of 25 ;-) might loom on other surprising occasions. And, last not least, TS simply computes a wrong result. It has shown unsound behavior. Can you trust TS’s type evaluation results under these circumstances any longer? Moreover, Microsoft’s reaction in person of their TS chief developer Ryan Cavanaugh was not really helpful, to my mind: He decrees that with a No answer to “extends”-questions you are always on the safe side or there is a fault in your program logic. Seriously? Draw your conclusion yourself. For the details: This is my bug report and I concluded indirectly from this related bug report that the “magic number” 25 is also the root problem in my case.

Design limitation 2: Incomplete inferences

I have stumbled over situations for several times now where TS does not make full use of the information that is available at compile time. Unfortunately, I cannot systematically describe the nature of these limitations. It would be nice if Microsoft did it or such descriptions existed elsewhere. I, for my part, am not aware of any documentation. If you know something, please let me know.

I give you one example of a situation where I miss the full evaluation of available information: Example code in playground. In line 96 TS complains about a type incompatibility: The first type parameter of generic type AND is constrained such that it allows instantiations with either type true or type false (but, in particular, not with type boolean). The parameter is instantiated with (the partially instantiated generic) type IsExactlyOneOf<T, U12> which, in turn, for every instantiation of T evaluates to either true or false. TS, somewhat superficially, concludes that IsExactlyOneOf<T, U12> evaluates, in general, to boolean and it therefore violates the the respective constraint for the first type parameter of AND. In reality, everything is fine with IsExactlyOneOf<T, U12> as first parameter for AND for any instantiation of T. Sure, this is a rather complicated inference but still I am disappointed and what is the general lesson? What can and cannot be done w.r.t. TS’s type inference? At least, the compiler does not refuse the creation of an executable JS program as clicking on the “Run” button in the playground proves.

Quirk: Use the infer syntax to create named type variables within conditional types for any type expression — wait: only almost any type expression

Consider you have a type definition of the sort

type T<U, V, W, X> =
ComplicatedType<V, W, U, V> extends OtherType<V, W>
? Modification<ComplicatedType<V, W, U, V>, X>
: ... ;

The point here is that you have a complex subexpression ComplicatedType<V, W, U, V> that occurs twice and, in particular if its evaluation is expensive, you want to avoid to evaluate it twice. What you can do is to cache the result in a type variable, say ComputationResult, that you create in the following way:

type Impossible = never;type T<U, V, W, X> =
ComplicatedType<V, W, U, V> extends infer ComputationResult
? (ComputationResult extends OtherType<V, W>
? Modification<ComputationResult, X>
: ... )
: Impossible;

To my mind this is quite an ugly syntax, to use a pseudo condition to make an assignment and to use keywords extends and infer that are deceiving, if they make any sense at all in this context. However, this is not my point. Note that it is a pseudo condition since the else branch is never entered. I have adopted the habit suggested in this article to use never as an error type to signal something that should not happen and to alias it with Impossible to make the intention clear.

To sum up, whatever ComplicatedType<V, W, U, V> evaluates to, the result is cached in a type variable named ComputationResult and the else branch is never entered, signalled by type Impossible. At least that was, what I thought until recently just to find out to my surprise that there is at least one exception to this rule: If ComplicatedType<V, W, U, V> evaluates to never, then variable creation “fails”, i.e. it is “impossible” for TS to assign never to the variable ComputationResult and to keep on with the evaluation. Instead, the computation enters the else branch of the ... extends infer ... clause. I don’t know what you think but I imagine here Ryan Cavanaugh telling me “Do not treat never as an ordinary type” with a moralizing undertone that I do not like at all.

Conclusion

To my mind, none of the three issues I have reported here is a No-Go, considered in isolation. However, I encounter such quirks again and again (I cannot document them all) and I wish I would have heard about them before I had started my project. Take this as a warning and think twice whether you want to use TS for serious projects, in particular, if you take typing seriously and want to exhaust the possibilities TS seems —to my mind too often only pretends — to have.

--

--

Responses (2)