翻译的打字稿对象上的条件类型和存在类型

Conditional and Existential Types on Translated Typescript Object

我希望这是有道理的。

我正在尝试编写一个函数,该函数接受一个对象,其中每个键等于一个函数,returns 一个函数的其余参数是该对象到 [对象的键, 以及该键函数的参数]

我的代码:

export const useConfig =
  <T extends Record<string, (...a: any) => null>>(features: T) =>
  <O extends T, K extends keyof O>(...args: [K, ...Parameters<O[K]>][]): void =>
    void 0;

const config = useConfig({
  attr: (prop: 'id', val: string) => null,
  style: (prop: 'font', val: 'arial') => null,
});

问题:

这个const t2 = config(['attr', 'font','']);不应该工作,它应该:

  1. 抱怨“字体”不对
  2. 将“attr”定义为第一个参数后,第二个参数应针对“prop:'id'”输入,第三个参数应针对“val:string”输入。

研发

  1. 我一直在尽力弄清楚这是否是存在类型问题(我相信是),但我无法建立联系
  2. 我知道这是条件类型的问题,因为如果我使用一堆可选参数为返回的函数创建多个泛型,每个参数都分配给每个泛型,它就可以工作。然而,每次我试图弄清楚如何“动态地”做到这一点时,我似乎都落在了“存在类型”上并且我绕了又绕。

如有任何帮助,我们将不胜感激。我觉得我只是没有了解类型系统的基本知识。

在下文中,我将调用有问题的操作,将键 Kextends keyof T 用于某些合适的 T,其值都是函数类型)转换为 tuple [K, ...Parameters<T[K]>]、“参数化”KK.

的“参数化”

从概念上讲,您希望 useConfig 的 return 类型是一个接受可变数量参数的函数,其中每个参数都是 some[=99= 的参数化] 输入 keyof T。你真的不想知道哪个参数是哪个键的参数化,只是每个参数对应于 some 键。这种“some”的使用确实暗示了 generic quantification you'd need here is existential instead of the "normal" universal quantification. You can think of normal, universal generics as intersections 在每个可接受的类型上,而存在泛型是 unions 在每个可接受的类型上。

而这里,由于“每个可接受的类型”只是keyof T的单个成员,那么你可以直接表示这个联合。您要做的就是 分配 K 中的联合进行参数化操作以创建新的联合。

如果你想在类键类型上分配操作,你可以使用分布式对象类型(如microsoft/TypeScript#47109) where you make a mapped type and then immediately index into it中创造的那样)。如果你有一个键集KS 并且您想在其上分配操作 F<K>,您可以这样写 {[K in KS]: F<K>}[KS]。在您的情况下 KSkeyof TF<K>[K, ...Parameters<T[K]>]。所以你得到这个:

const useConfig =
<T extends Record<string, (...a: any) => null>>(features: T) =>
  (...args: { [K in keyof T]-?: [K, ...Parameters<T[K]>] }[keyof T][]): void =>
    void 0;

让我们看看当我们调用它时会发生什么:

const config = useConfig({
  attr: (prop: 'id', val: string) => null,
  style: (prop: 'font', val: 'arial') => null,
});

/* const config: (...args: (
  ["attr", "id", string] | ["style", "font", "arial"]
)[]) => void */

所以这正是您想要的。传递给 config() 的每个参数应该是 ["attr", "id", string] 类型的元组或 ["style", "font", "arial"] 类型之一。你会得到你关心的类型检查:

const t = config(
  ["attr", "id", "abc"], // okay
  ["style", 'font', "arial"], // okay
  ["attr", "font", ""] // error!
  //~~~~~~~~~~~~~~~~~~
);

遗憾的是,这并不能为您提供出色的 IntelliSense 和自动完成体验。这并不是上述解决方案的真正问题,而是 TypeScript 的限制或缺失功能,在 microsoft/TypeScript#38603.

处请求

我们可以通过让编译器实际尝试找出哪个参数是哪个键的参数化来解决这个问题。这意味着如果我们调用

const t = config(
  ["attr", "id", "abc"], 
  ["style", 'font', "arial"], 
  ["attr", "font", "abc"]
);

编译器需要知道“第一个键是"attr",第二个是"style",第三个是"attr"。如果你把它想象成一个元组keysKS,那么这里的KS就是["attr", "style", "attr"],而我们需要将KS元组的操作表示为元组中每个key的参数化的元组,即就是,我们想map对输入元组进行参数化操作,得到一个输出元组。

那个版本看起来像这样:

const useConfig =
<T extends Record<string, (...a: any) => null>>(features: T) =>
  <KS extends Array<keyof T>>(...args: {
    [I in keyof KS]: [KS[I], ...Parameters<T[Extract<KS[I], keyof T>]>]
  }): void =>
    void 0;

有点复杂;编译器确实将元组映射到 {[I in keyof KS]: F<I>} 仅作用于 numeric-like 索引 I 的元组,但它不太清楚它是在映射类型的主体内执行此操作,因此它如果您将 KS[I] 视为 keyof T,将会犹豫不决(因为“wHaT iF I 是某种类似于 "push" 的方法?请参阅 ms/TS#27995). We need to use the Extract<T, U> utility type 以说服KS[I] 可以被视为可分配给 keyof T.

的编译器

现在当我们调用 config():

const t = config(
  ["attr", "id", "abc"], // okay
  ["style", 'font', "arial"], // okay
  ["attr", "font", "abc"], error!
  // -----> ~~~~ <---- error here
);

/* const config: <["attr", "style", "attr"]>(
    args_0: ["attr", "id", string], 
    args_1: ["style", "font", "arial"], 
    args_2: ["attr", "id", string]) => void 
*/

您可以看到编译器为 KS 推断出 ["attr", "style", "attr"],然后特别不满 args_2 参数中的无效 "font" 值。

Playground link to code