Suggestion: Provide a way to force simplification/normalization of conditional and mapped types
See original GitHub issueSuggestion
đ Search Terms
simplification normalization normal form eager evaluation
The closest I found to this was https://github.com/microsoft/TypeScript/issues/20267
Eager normalize normalization simplify
â Viability Checklist
My suggestion meets these guidelines:
- This wouldnât be a breaking change in existing TypeScript/JavaScript code
- This wouldnât change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isnât a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScriptâs Design Goals.
On the design goal point, I believe this feature would further goals 5 and 9:
- Produce a language that is composable and easy to reason about.
- Use a consistent, fully erasable, structural type system.
Elaboration on this point is below.
â Suggestion
Add a way to force eager evaluation of mapped and conditional types, or a way to force evaluation of a type until it reaches a sum-of-products normal(ish) form. This would lead to simpler inferred types from a userâs perspective when type algebra is in play, as well as more terse and focused error messages.
I think there are multiple ways this could be done. There could be an eager
keyword which, when attached to a conditional or mapped type, causes that type to be evaluated as soon as possible, discarding intermediate type information. It could be a baked-in âutility typeâ, like Simplify<T>
or Simplify<T, depth extends number>
which, when applied to a type, forces the evaluation of that type (until it reaches a sum-of-products form, or for a given number of steps).
đ Motivating Example
Here is a small example of a case where a utility type leads to much less useful intellisense information than would be present with a manually-written type. My proposal here would be a way to force the type AtLeastOneBar
, which has inferred type
(Required<Pick<Bar, "one">> & Partial<Pick<Bar, "two" | "three">>) |
(Required<Pick<Bar, "two">> & Partial<...>) | (Required<...> & Partial<...>
to simplify to the equivalent type,
{
one: number,
two?: number,
three?: number
} | {
one?: number,
two: number,
three?: number
} | {
one?: number,
two?: number,
three: number
}
This case may be a matter of personal taste, but I chose it because it is comparatively simple. It may be that users would prefer to prevent any expansion of this type at all, and have the following inferred type:
RequireAtLeastOne<{
one: number,
two: number,
three: number
}>
I think this difference in preferences is a valid point of discussion, because itâs also on the topic of âhow can library authors exercise more control over the types produced by their utilities?â
Here is a much more advanced example, and a case where I donât think the need for simpler types is as subjective. This is a simplified excerpt from a library which I am building, which aims to do a similar job to Runtypes/Zod/etc., but with extra extensibility. The type algebra in the library is hairy, and I do not know whether I will complete it because the complexity of the inferred types is too high for the library to be practically useful.
In this example, I define classes to describe schemas at runtime and link them to TS types. I show two types, which are equivalent. (They are subtypes of each other.) One is the manually created type,
{
a: boolean,
b?: boolean
}
and one is the library-inferred type
Partial<Unwrap<{
a: Schema<boolean, {}>;
b: Schema<boolean, Record<"optional", true>>;
}>> & Required<Pick<Unwrap<{
a: Schema<boolean, {}>;
b: Schema<boolean, Record<"optional", true>>;
}>, "a">
The inferred type leaks implementation details to all of the libraryâs users. It makes the types way harder to understand and leads to intimidating type errors. The goal of this issue is to provide a way to make the actual type normalize down to the desired type inside of the library.
đť Use Cases
I believe this feature would address a common complaint about use of TSâs advanced features: âAvoid type algebra because it adds complexity and is hard to reason aboutâ. From the typecheckerâs perspective, types compose. From a userâs perspective, reasoning about types is not compositional due to progressively increasing complexity. Itâs pretty easy to use type algebra to make several types which are comprehensible on their own, but which are incomprehensible when composed. As a result, some users (very persuasively) advise against using type algebra anywhere, because doing so can lead to complexity which ripples across a codebase.
In my opinion the problem is that itâs not possible to write abstractions which encapsulate the complexity of type algebra. The situation is like if JS was lazily evaluated, and if using console.log
on a value for debugging printed out a long series of thunks reflecting all of the computations which lead to that value instead of the value itself. If we could force type algebra to be simplified, it could make type-level programming easier to debug and maintain, and it would allow library authors to hide the fact that type algebra is occurring at all from end-users.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:2
- Comments:6 (3 by maintainers)
Top GitHub Comments
This (at time of writing) works:
If needed a recursive version and a real-world example, look here: Playground
And I have a suggestion to add such util to the default Utility Types, if not planned to solve this issue in the compiler.