Home > Software design >  What ahead-of-time generation actually means in typescript handbook?
What ahead-of-time generation actually means in typescript handbook?

Time:12-05

In the typescript handbook-template literal types, it said:

For each interpolated position in the template literal, the unions are cross multiplied:

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt"; 
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;

Then it said:

We generally recommend that people use ahead-of-time generation for large string unions, but this is useful in smaller cases.

But what is "ahead-of-time generation"? And what last paragraph means? I can't quite understand since he didn't give an example.

CodePudding user response:

In order to get something like an authoritative answer I looked up the commit that introduced this documentation, traced it back to the commit of the release notes for TypeScript 4.1 with similar wording:

when you need a ton of strings, you should consider automatically generating them ahead of time to save work on every type-check

Both of these commits were authored by @orta. I asked him on the TypeScript Discord where that came from orignally, and it's basically from the TS dev blog written by @DanielRosenwasser, but @orta edits the text also.


So, "ahead-of-time generation" is the act of writing some code in a language of your choice (this could be TypeScript but it doesn't have to be), that generates your desired TypeScript code when you run it ahead of time, once.

So instead of writing code directly that does X, you write code that writes code that does X.

The suggestion here is that instead of writing the following TypeScript code,

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;

where the compiler has to evaluates the LocaleMessageIDs union itself, you leave your TypeScript IDE and go to some other environment (say a different TypeScript IDE) and write this:

const locales = ["welcome_email", "email_heading", "footer_title", "footer_sendoff"];
const langs = ["en", "ja", "pt"];
console.log(
    "type LocaleMessageIDs = "  
    langs.map(lang =>
        locales.map(locale =>
            "\""   lang   "_"   locale   "_id\""
        ).join(" | ")
    ).join(" |\n    ")   ";"
);

When you run this code, the console logs the following:

type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" |
    "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" |
    "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id";

which you can copy and paste into your original TypeScript IDE.

Playground link to code

In both cases, you end up with a LocaleMessageIDs type which is a union of the same twelve string literal types. But in the first case the TypeScript compiler is evaluating the union by processing template literal types, while in the second case the TypeScript compiler only has to parse the string literals directly.


For simple and small unions like LocaleMessageID it's pretty silly to do any sort of code generation. But it's easy to change that into something that generates unions that are complicated enough to calculate that the compiler performance is seriously degraded.

Here's an intentionally suboptimal implementation of a type called PalindromicFourteenBitString, any string consisting of just "0" and "1" which is the same forwards as backwards:

type Binary = '1' | '0';

type BinaryLength<N extends number, L extends 0[] = [], O extends string = ""> =
    N extends L['length'] ? O : BinaryLength<N, [0, ...L], `${O}${Binary}`>;

type Reverse<T extends string, C extends string = ""> = 
    T extends `${infer F}${infer R}` ?
    Reverse<R, `${F}${C}`> : C;

type PalindromicFourteenBitString = BinaryLength<14> extends infer B ?
    B extends Reverse<Extract<B, string>> ? B : never : never;

It's a suboptimal implementation because we generate every single binary string of length 14 and filter out any that are not palindromes. There is a much better way to do it (generate strings of length 7 and mirror each one) but the point is to come up with something that takes some computation to evaluate.

Anyway, when you go to test it, you'll probably notice that the compiler is slow to point out the line with the error in it, and if you look at your computer's CPU it might be working quite hard:

let b: PalindromicFourteenBitString; //            
  • Related