在打字稿手册中,提前生成实际上意味着什么?

Cho*_*hor 9 typescript

打字稿手册模板文字类型中,它说:

对于模板文字中的每个插值位置,并集被交叉相乘:

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt"; 
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
Run Code Online (Sandbox Code Playgroud)

然后它说:

我们通常建议人们对大型字符串联合使用提前生成,但这在较小的情况下很有用。

但什么是“超前一代”?最后一段是什么意思?我不太明白,因为他没有举例。

jca*_*alz 11

为了获得类似权威答案的内容,我查找了引入此文档的提交,并将其追溯到具有类似措辞的 TypeScript 4.1 发行说明的提交

当您需要大量字符串时,您应该考虑提前自动生成它们,以节省每次类型检查的工作

这两个提交都是由 @orta 创作的。我在 TypeScript Discord 上问他这个内容最初来自哪里,它基本上来自@DanielRosenwasser 撰写的TS 开发博客,但 @orta 也编辑了文本。


因此,“提前生成”是用您选择的语言(可能是 TypeScript,但不一定是)编写一些代码的行为,当您提前运行它时,会生成您想要的 TypeScript 代码。一次,一次。

因此,您不是直接编写执行 X 的代码,而是编写执行 X 的代码。

这里的建议是,不要编写以下 TypeScript 代码,

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}`;
Run Code Online (Sandbox Code Playgroud)

当编译器必须评估LocaleMessageIDs联合本身时,您可以离开 TypeScript IDE 并转到其他环境(例如不同的 TypeScript IDE)并编写以下内容:

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    ") + ";"
);
Run Code Online (Sandbox Code Playgroud)

当您运行此代码时,控制台会记录以下内容:

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";
Run Code Online (Sandbox Code Playgroud)

您可以将其复制并粘贴到原始 TypeScript IDE 中。

Playground 代码链接

在这两种情况下,您最终都会得到一个LocaleMessageIDs类型,该类型是相同的十二个字符串文字类型的联合。但在第一种情况下,TypeScript 编译器通过处理模板文字类型来评估联合,而在第二种情况下,TypeScript 编译器只需直接解析字符串文字。


对于像这样的简单和小型联合,LocaleMessageID进行任何类型的代码生成都是相当愚蠢的。但很容易将其更改为生成复杂到足以计算出编译器性能严重下降的联合的东西。

这是一个名为 的类型的故意次优实现,任何由 just和PalindromicFourteenBitString组成的字符串,其向前与向后相同:"0""1"

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;
Run Code Online (Sandbox Code Playgroud)

这是一个次优的实现,因为我们生成长度为 14 的每个二进制字符串并过滤掉任何非回文字符串。有一种更好的方法可以做到这一点(生成长度为 7 的字符串并镜像每个字符串),但重点是提出需要一些计算才能评估的东西。

不管怎样,当你去测试它时,你可能会注意到编译器很慢地指出其中存在错误的行,并且如果你查看计算机的 CPU,它可能会非常努力地工作:

let b: PalindromicFourteenBitString; //  
b = "10010011001001"; // okay
b = "10001011001001"; // error
b = "00110011001100"; // okay
Run Code Online (Sandbox Code Playgroud)

Playground 代码链接

将其与进行代码生成时发生的情况进行比较:

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(" | "));
Run Code Online (Sandbox Code Playgroud)

如果你运行它,它会输出以下内容(而且很快):

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";
Run Code Online (Sandbox Code Playgroud)

如果您将该输出作为 TS 源代码并对其进行测试,则编译器性能非常快,并且没有 CPU 峰值可言:

let b: PalindromicFourteenBitString;  
b = "10010011001001"; // okay
b = "10001011001001"; // error
b = "00110011001100"; // okay
Run Code Online (Sandbox Code Playgroud)

Playground 代码链接

因此,在这个示例中,您将获得更好的计算体验,提前联合自己,而不是要求 TypeScript 编译器通过模板文字类型操作来完成此操作。


当我问他该文档行的含义时,@orta 详细说明了不应在模板字符串文字类型中编写大型类型函数,例如将GraphQL 查询字符串转换为相应的类型。如果您想编写代码来执行此操作,则应该使用其他语言编写它,并从其输出生成 TypeScript 类型。


旁白:其他糟糕的想法,其中滥用编译器来评估类型比代码生成更糟糕