国际象棋记谱法:TypeScript 模板字符串:TS2590:表达式产生的联合类型复杂度过高,无法表示
我正在尝试用 TypeScript 模板字符串表示代数象棋记谱法。
我使用的类型:
export type ChessPiece = 'P' | 'K' | 'Q' | 'R' | 'B' | 'N';
export type ChessX = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h';
export type ChessY = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type AlgebraicMoveNotationDisambiguate = '' | ChessX | ChessY | `${ChessX}${ChessY}`;
export type AlgebraicMoveNotationBase = `${ChessPiece | ChessX}${AlgebraicMoveNotationDisambiguate}${'x' | ''}${ChessX}${ChessY}`;
export type AlgebraicMoveNotationPromote = `${ChessX}8${ChessPiece}`;
export type AlgebraicMoveNotationDecoration = `${'' | '+' | '#'}`;
export type AlgebraicMoveNotation = 'O-O' | 'O-O-O' | `${AlgebraicMoveNotationBase|AlgebraicMoveNotationPromote}${AlgebraicMoveNotationDecoration}`;
这在第6行(AlgebraicMoveNotationBase)产生了一个错误:
TS2590: Expression produces a union type that is too complex to represent.
我理解这是一个相当复杂的表达式,并且一直计算所有可能的值可能不可行(我做过计算,最终类型 AlgebraicMoveNotation 大约有 35 亿种组合)。
我的问题是——有没有办法配置 TS,只评估已分配的值,而不尝试理解所有可能的类型,例如:
// Example 1 =======
const someMove: AlgebraicMoveNotation = 'exd5'; // <- Checks provided literal is valid
// Example 2 =======
const validateMoves(moves: AlgebraicMoveNotation[]): void {
// ...
}
validateMoves([ // <- Checks provided literals in array are valid
'Nf6+',
'a8Q',
'Qexe1#',
]);
我试着检查类似的错误,但没有找到任何相关内容。
更简单的示例以便更容易复现:
export type ChessPiece = 'P' | 'K' | 'Q' | 'R' | 'B' | 'N';
export type ChessX = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h';
export type ChessY = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type AlgebraicMoveNotationDisambiguate = '' | ChessX | ChessY | `${ChessX}${ChessY}`;
export type AlgebraicMoveNotationCapture = '' | 'x';
export type AlgebraicMoveNotationBase = `${ChessPiece | ChessX}${AlgebraicMoveNotationDisambiguate}${AlgebraicMoveNotationCapture}${ChessX}${ChessY}`;
答案#1
我不会讲象棋走步的具体类型函数,那已经在另一个答案中写过了,我将讨论一般的这一类问题。
TypeScript 无法表示拥有约 100,000 名成员的工会类型。 这对于很多用途来说已经足够了,但当 microsoft/TypeScript#40336 引入模板字面类型时,通过将字符串-字面型的并集在一起,可以轻松生成庞大的并集。 如果你有一个类型 M,它是 m 个字符串字面类型组成的并,另一个类型 N 是 n 个字符串字面类型组成的并,那么模板的字面类型 '$ {M}$ {N}' 将是 m×n 个字符串文字类型的并。 随着你重复串接,这种情况很快就会失控:把 k 个 N 的副本串联在一起,你就会得到 nk 个字符串的并集。 即使是小巧的n和k,你也会很快超过语言代表联合体的能力。
像大多数这种语言限制一样,这是不可配置的。一般来说,它们被认为是编译器的实现细节,并不打算让用户去调整。(例如,见 microsoft/TypeScript#45025 描述末尾。)大型联合类型本身就对编译器资源要求很高,增加限制几乎肯定只会让情况更糟,因此你可以预期编译器会变得无法使用或者崩溃,而不是发出警告。所以,这不是一个可行的选项。
相反,你通常需要一种不同的方法。如果你只是想检查单个字符串字面量类型是否有效,那么在一个巨大的联合类型中生成每一个可能的有效字符串字面量类型是不必要的。检查是否属于集合在概念上可能很简单,但实现一个谓词往往要容易得多。(如果我问你 "abc" 是否有三个字母,你 不想先生成所有三个字母单词的集合。)
这意味着你可以从
type ValidString = ⋯;
到类似的通用类型
type ValidateString<T extends string> = ⋯;
当且仅当 T extends ValidString 为真时,T extends ValidateString<T> 才成立。 然后 Validate<T> 字符串像对类型 T 的约束一样。 为了避免自己写出类型参数,你可以创建一个帮助函数来推断它。
理想的辅助函数看起来应该是
const validateString =
<T extends ValidateString<T>>(t: T) => t;
但这通常会触发循环检测器。你可以将其改写为类似这样的句子
const validateString =
<T extends string>(t: T & ValidateString<T>) => t;
甚至
const validateString = <T extends string>(
t: T extends ValidateString<T> ? T : ValidateString<T>
) => t;
这将允许编译器根据 t 的值推断 T,然后使用 ValidateString<T> 进行检查。
因此,而不是写
const str: ValidString = "⋯"; // need huge union
你会写
const str = validateString("⋯"); // only check the passed-in string
这并没有太大的不同。
至于将大联合类型转换为约束的一般方法,你可以使用条件类型推断来解析你的候选字符串。如果你有类似的东西
type ValidString = `${M}${N}${O}`
你可能会把它改成
type ValidateString<T extends string> =
T extends `${M}${infer _NO}` ?
_NO extends `${N}${infer __O}` ?
__O extends `${O}` ? T :
never : never : never;
这会将字符串分割成若干部分,首先查看它是否是 M,后面跟着我们称之为 _NO 的其余字符串……然后查看 _NO 是否是 N,后面跟着我们称之为 __O 的其余字符串,最后再查看 __O 是否最终是 O。每一步只处理 M、N 和 O 尺寸的联合,而不将它们相乘。
有一些替代 never 的方法可以使错误信息更容易接受,但它们会变得更难编写。目标是让 ValidateString<T> 生成与 T “最接近”的有效字符串,根据某种“接近”的启发式概念。这样,当你调用 validateString("inkorrekt") 时,编译器将给出类似 '"inkorrekt" 不能赋值给 "correct"' 的错误信息,希望能促使你修正它。我不会详细说明如何处理象棋示例或一般问题……关键问题在于,寻找失败的部分匹配可能会创建联合类型,而你不希望输出中出现大的联合类型,否则你有可能再次触及限制。
代码的 Playground 链接
答案#2
感谢 @kelsny 和 @jcalz!
我能够想出这个解决方案:
export type ChessPiecePromotes = 'Q' | 'R' | 'B' | 'N';
export type ChessPieceWithoutPawn = 'K' | ChessPiecePromotes;
export type ChessPiece = 'P' | ChessPieceWithoutPawn;
export type ChessX = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h';
export type ChessY = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type AlgebraicMoveNotationTarget = `${ChessX}${ChessY}`
export type AlgebraicMoveNotationDisambiguate = '' | ChessX | ChessY | AlgebraicMoveNotationTarget;
export type AlgebraicMoveNotationCapture = '' | 'x';
export type AlgebraicMoveNotationPromote = `${ChessX}8${ChessPiecePromotes}`;
export type AlgebraicMoveNotationDecoration = '' | '+' | '#';
export type AlgebraicMoveNotationCastle = 'O-O' | 'O-O-O';
export type CheckAlgebraicMoveNotation<T extends string> =
|
T extends `${ChessX}${infer C}` ?
C extends `${AlgebraicMoveNotationCapture}${infer G}` ?
G extends `${AlgebraicMoveNotationTarget}${infer E}` ?
E extends AlgebraicMoveNotationDecoration ? T
: never : never : never : never
|
T extends `${ChessPieceWithoutPawn}${infer D}` ?
D extends `${AlgebraicMoveNotationDisambiguate}${infer C}` ?
C extends `${AlgebraicMoveNotationCapture}${infer G}` ?
G extends `${AlgebraicMoveNotationTarget}${infer E}` ?
E extends AlgebraicMoveNotationDecoration ? T
: never : never : never : never : never
| T extends AlgebraicMoveNotationPromote ? T : never
| T extends AlgebraicMoveNotationCastle ? T : never
;
export function notation<T extends string>(move: T extends CheckAlgebraicMoveNotation<T> ? T : never) {
return move;
}
