Typescript - [MyType<T>,推断 T] 的数组,其中 T 可以因元素而异

Typescript - Array of [MyType<T>, inferred T] where T can vary per element

TL;DR

如何创建一个元组数组,其中元组的第二个元素是从第一个元素推断出来的?

这是下面代码的typescript playground

我的情况

我正在使用 Puppeteer 打开各种数独网站,阅读游戏,解决问题,然后转到下一个网站。

每个站点都由 Puppet<TDiff> 表示,其中 TDiff 是一组字符串,表示站点特定的难度名称(如果有的话)。我有一个名为 runPuppet 的函数,它接受 Puppet 和一些选项,其中之一是难度,然后执行上述算法。

(很抱歉代码片段太长。我尽量保持简短。)

运行-puppet.ts

interface Puppet<TDiff extends string | undefined = undefined> {
   // ...
}

interface RunOptions<TDiff extends string | undefined> {
   difficulty: TDiff;
   newGame: boolean;
   // ...
}

const default options = {
   newGame: false,
   // difficulty is not defined
   // ...
};

export default async function runPuppet<TDiff extends string | undefined = undefined>(
    puppet: Puppet<TDiff>,
    options: Partial<RunOptions<TDiff>>
) {
    options = Object.assign({}, defaultOptions, options);
    // ...
}

数独-com.ts(对于sudoku.com

type Difficulty = 'easy' | 'medium' | 'hard' | 'expert';

const SudokuDotComPuppet: Puppet<Difficulty> = {
    // ...
}

websudoku-com.ts (for websudoku.com)

type Difficulty = 'easy' | 'medium' | 'hard' | 'evil';

const WebSudokuDotComPuppet: Puppet<Difficulty> = {
    // ...
}

main.ts

(async () => {
    await runPuppet(WebSudokuDotComPuppet, { difficulty: 'evil' });
    await runPuppet(WebSudokuDotComPuppet, { difficulty: 'evil', newGame: true });
    await runPuppet(WebSudokuDotComPuppet, { difficulty: 'evil', newGame: true });
    await runPuppet(SudokuDotComPuppet, { difficulty: 'expert' });
    await runPuppet(SudokuDotComPuppet, { difficulty: 'expert', newGame: true });
    await runPuppet(SudokuDotComPuppet, { difficulty: 'expert', newGame: true });
})();

此代码 运行 两个木偶都运行了三次并且一切正常。

现在我想抽象化 main.ts 中的代码以使用元组数组:Array<[Puppet<TDiff>, TDiff]> 其中每个元素都有自己的 TDiff。这样我可以做到:

// needs to be fixed
type PuppetDifficulty<TDiff extends string | undefined = undefined> =
    [Puppet<TDiff>, TDiff];

(async () => {
    // throws compile-time errors
    const puppets: PuppetDifficulty[] = [
        [ WebSudokuDotComPuppet, 'evil' ],
        [ SudokuDotComPuppet, 'expert' ],
    ];

    for (const [puppet, difficulty] of puppets) {
        for (let i = 0; i < 3; i++) {
            await runPuppet(puppet, { difficulty, newGame: !!i });
        }
    }
})();

这会引发四个错误,这些错误基本上都可以归结为 'expert''evil' 无法分配给 undefined。这是因为没有 <TDiff>Puppet 假定它是未定义的,而不是试图根据参数进行推断。

我尝试使用 ElementType<T> 模式:

type DifficultyType<TPuppet extends Puppet> =
    TPuppet extends Puppet<infer T> ? T : undefined;

type PuppetDifficulty<TPuppet extends Puppet = Puppet> = [ TPuppet, DifficultyType<TPuppet> ];

(async () => {
    const puppets: PuppetDifficulty[] = [
        [ WebSudokuDotComPuppet, 'evil' ],
        [ SudokuDotComPuppet, 'expert' ],
    ];

    // ...
)();

这会导致同样的四个错误。

我不知道我是否有时间完全解释这个,但作为一个草图..基本的想法是制作一个泛型辅助函数,其泛型类型参数是旨在成为 tuple corresponding to the first parameter of each of your pairs, and then map this tuple type to the actual pairs you are passing in. The compiler should be able to use inference from mapped types 以从 passed-in 数组推断类型参数,或者在您执行无法推断的操作时警告您。关于提示编译器将 ["a", 1] 之类的参数解释为 [string, number] 而不是 Array<string | number>、如何获得正确的推理等等,有各种各样的警告,但这是基本思想。这是一种方法:

const puppetDifficulties = <P extends Array<Puppet<any>>>(
  arr: [] | { [I in keyof P]: [P[I], P[I] extends Puppet<infer S> ? S : never] }
) => arr as Exclude<typeof arr, []>;

然后我会这样使用它:

  const puppets = puppetDifficulties([
    [Puppet1, "expert"],
    [Puppet2, "evil"],
    [Puppet1, "XXXX"] // error! not assignable to [Puppet<Puppet1Diff>, Puppet1Diff]
  ]);

请注意,尽管 puppets 是强类型的,但通过将其作为常规数组进行迭代会失去类型安全性(迭代 [string, number, boolean] 类型的元组只会为您提供元素输入 string | number | boolean。所以下面不会给你任何错误,而只是因为事情被解释为联合:

  for (const [puppet, difficulty] of puppets) {
    for (let i = 0; i < 3; i++) {
      runPuppet(puppet, { difficulty, newGame: !!i });
    }
  }

好的,希望对您有所帮助;祝你好运!

Link to code