Vad betyder egentligen förhandsgenerering i TypeScript-handboken?


I handboken för TypeScript om mallsträngstyper stod det:

För varje interpolerad position i mallsträngen multipliceras unionerna korsvis:

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

Sedan sa det:

Vi rekommenderar generellt att människor använder förhandsgenerering för stora strängunioner, men detta är användbart i mindre fall.

Men vad är "ahead-of-time generation"? Och vad betyder sista stycket? Jag kan inte riktigt förstå eftersom han inte gav något exempel.

Svar #1

För att få något som ett auktoritativt svar tittade jag upp commiten som införde denna dokumentation, spårade den tillbaka till commiten för release-anteckningarna för TypeScript 4.1 med liknande formulering:

när du behöver en massa strängar bör du överväga att automatiskt generera dem i förväg för att spara arbete vid varje typkontroll

Båda dessa commits skrevs av @orta. Jag frågade honom på TypeScript Discord var det kom ifrån ursprungligen, och det kommer i princip från TS dev-bloggen skriven av @DanielRosenwasser, men @orta redigerar också texten.


Så, "ahead-of-time generation" är handlingen att skriva någon kod i ett språk du väljer (detta kan vara TypeScript men det behöver inte vara det), som genererar din önskade TypeScript-kod när du kör den i förväg, en gång.

Så istället för att skriva kod direkt som gör X, skriver du kod som skriver kod som gör X.

Förslaget här är att istället för att skriva följande TypeScript-kod,

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}`;

där kompilatorn själv måste utvärdera LocaleMessageIDs-unionen, lämnar du din TypeScript-IDE och går till någon annan miljö (säg en annan TypeScript-IDE) och skriver detta:

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    ") + ";"
);

När du kör den här koden loggar konsolen följande:

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";

som du kan kopiera och klistra in i din ursprungliga TypeScript-IDE.

Lekplatslänk till kod

I båda fallen hamnar du med en LocaleMessageIDs-typ som är en union av samma tolv strängliteraltyper. Men i det första fallet utvärderar TypeScript-kompilatorn unionen genom att bearbeta template literal-typer, medan i det andra fallet behöver TypeScript-kompilatorn bara analysera strängliteralerna direkt.


För enkla och små unioner som LocaleMessageID är det ganska dumt att göra någon form av kodgenerering. Men det är lätt att ändra det till något som genererar unioner som är tillräckligt komplicerade för att beräkna att kompilatorns prestanda försämras allvarligt.

Här är en avsiktligt suboptimal implementation av en typ som kallas PalindromicFourteenBitString, vilken som helst sträng bestående av enbart "0" och "1" som är samma framåt som bakåt:

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;

Det är en suboptimal implementering eftersom vi genererar varje enskild binär sträng av längd 14 och filtrerar bort alla som inte är palindromer. Det finns ett mycket bättre sätt att göra det på (generera strängar av längd 7 och spegla varje), men poängen är att komma på något som tar viss beräkning för att utvärdera.

Hur som helst, när du går för att testa det kommer du förmodligen att märka att kompilatorn är långsam med att peka ut raden med felet, och om du tittar på din dators CPU kan den arbeta ganska hårt:

let b: PalindromicFourteenBitString; // 🐢 🔥💻🔥
b = "10010011001001"; // okay
b = "10001011001001"; // error
b = "00110011001100"; // okay

Playground-länk till kod

Jämför detta med vad som händer om du gör kodgenerering:

const palindromicFourteenBitStrings =
    Array.from({ length: 1 << 14 }, (_, i) => i.toString(2).padStart(14, "0"))
        .filter(s => s === s.split("").reverse().join(""));
console.log(
    "type PalindromicFourteenBitString =\n    " +
    palindromicFourteenBitStrings.map((x, i) => '"' + x + '"' +
        (i % 8 == 7 ? "\n   " : "")).join(" | "));

Om du kör det så ger det följande utdata (och ganska snabbt):

type PalindromicFourteenBitString =
    "00000000000000" | "00000011000000" | "00000100100000" | "00000111100000" | "00001000010000" | "00001011010000" | "00001100110000" | "00001111110000"
    | "00010000001000" | "00010011001000" | "00010100101000" | "00010111101000" | "00011000011000" | "00011011011000" | "00011100111000" | "00011111111000"
    | "00100000000100" | "00100011000100" | "00100100100100" | "00100111100100" | "00101000010100" | "00101011010100" | "00101100110100" | "00101111110100"
    | "00110000001100" | "00110011001100" | "00110100101100" | "00110111101100" | "00111000011100" | "00111011011100" | "00111100111100" | "00111111111100"
    | "01000000000010" | "01000011000010" | "01000100100010" | "01000111100010" | "01001000010010" | "01001011010010" | "01001100110010" | "01001111110010"
    | "01010000001010" | "01010011001010" | "01010100101010" | "01010111101010" | "01011000011010" | "01011011011010" | "01011100111010" | "01011111111010"
    | "01100000000110" | "01100011000110" | "01100100100110" | "01100111100110" | "01101000010110" | "01101011010110" | "01101100110110" | "01101111110110"
    | "01110000001110" | "01110011001110" | "01110100101110" | "01110111101110" | "01111000011110" | "01111011011110" | "01111100111110" | "01111111111110"
    | "10000000000001" | "10000011000001" | "10000100100001" | "10000111100001" | "10001000010001" | "10001011010001" | "10001100110001" | "10001111110001"
    | "10010000001001" | "10010011001001" | "10010100101001" | "10010111101001" | "10011000011001" | "10011011011001" | "10011100111001" | "10011111111001"
    | "10100000000101" | "10100011000101" | "10100100100101" | "10100111100101" | "10101000010101" | "10101011010101" | "10101100110101" | "10101111110101"
    | "10110000001101" | "10110011001101" | "10110100101101" | "10110111101101" | "10111000011101" | "10111011011101" | "10111100111101" | "10111111111101"
    | "11000000000011" | "11000011000011" | "11000100100011" | "11000111100011" | "11001000010011" | "11001011010011" | "11001100110011" | "11001111110011"
    | "11010000001011" | "11010011001011" | "11010100101011" | "11010111101011" | "11011000011011" | "11011011011011" | "11011100111011" | "11011111111011"
    | "11100000000111" | "11100011000111" | "11100100100111" | "11100111100111" | "11101000010111" | "11101011010111" | "11101100110111" | "11101111110111"
    | "11110000001111" | "11110011001111" | "11110100101111" | "11110111101111" | "11111000011111" | "11111011011111" | "11111100111111" | "11111111111111";

Om du placerar den utdata som din TS-källkod och testar det, är kompilatorns prestanda mycket snabb och det finns ingen CPU-ökning att tala om:

let b: PalindromicFourteenBitString; 🐇 🧊💻🧊
b = "10010011001001"; // okay
b = "10001011001001"; // error
b = "00110011001100"; // okay

Lekplatslänk till kod

Så detta är ett exempel där du kommer att få en mycket bättre upplevelse om du beräknar den unionen själv i förväg istället för att be TypeScript-kompilatorn göra det via mallsträngstypmanipulation.


När jag frågade honom vad den där dok-linjen betydde, förklarade @orta att man inte borde skriva en stor typfunktion i mallsträngslitteraler för typer för att, säg, omvandla en GraphQL-frågesträng till en motsvarande typ. Om du vill skriva kod för att göra det, borde du skriva den i något annat språk och generera TypeScript-typerna från resultatet av det.


  • tuplen av primtal som är mindre än eller lika med ett visst värde

Relaterade artiklar