question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Suggestion: Provide a way to force simplification/normalization of conditional and mapped types

See original GitHub issue

Suggestion

🔍 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:open
  • Created 2 years ago
  • Reactions:2
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

3reactions
RyanCavanaughcommented, Feb 23, 2022

This (at time of writing) works:

type Expand<T> = T extends unknown ? { [K in keyof T]: T[K] } : never;
type Out = Expand<AtLeastOneBar>;
2reactions
ModPhoenixcommented, Oct 19, 2022

If needed a recursive version and a real-world example, look here: Playground

/**
 * Force expand (simplification/normalization) of conditional and mapped types.
 * More info {@link https://github.com/microsoft/TypeScript/issues/47980 TypeScript/issues/47980}
 */
export type Expand<T> = T extends unknown
  ? { [K in keyof T]: Expand<T[K]> }
  : never;

And I have a suggestion to add such util to the default Utility Types, if not planned to solve this issue in the compiler.

Read more comments on GitHub >

github_iconTop Results From Across the Web

https://tools.ietf.org/id/draft-ietf-precis-mappin...
This document defines some mappings in these mapping types. ... normalization method, fullwidth/halfwidth characters are mapped into its compatible form.
Read more >
Score Normalization - an overview | ScienceDirect Topics
Data normalization involves scaling the attribute values to make them lie numerically in the same interval/scale, and thus have the same importance. Because ......
Read more >
Make your React components great with TypeScript mapped ...
Let us see how we can mix conditional types with mapped types to infer a new type from Person which only includes the...
Read more >
Extensible Markup Language (XML) 1.1 (Second Edition) - W3C
This specification describes the required behavior of an XML processor in terms of how it must read XML data and the information it...
Read more >
The Logic of Conditionals - Stanford Encyclopedia of Philosophy
This article provides a survey of classic and recent work in conditional logic. We review the problems of a two-valued analysis and examine ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found