我想定义一个类型NodeDef,它定义了图执行框架中的一个 node .每个NodeDef具有name、其依赖(其输入)的 node 的列表deps以及基于输入来计算其输出的函数compute.

我希望将compute函数的参数限制为将deps下指定的字符串文字作为属性的对象:

type Nodes = {
    a: number;
    b: number;
    c: string;
};

type Node = keyof Nodes;

type NodeDef<N extends Node, D extends Node> = {
    name: N;
    deps: D[];
    compute: (deps: { [K in D]: Nodes[K] }) => Nodes[N];
};

type Graph = {
    [K in Node]?: NodeDef<K, Node>;
};

但是,当我创建图形时,配置计算函数的类型没有正确提供:

const config: Graph = {
    a: {
        name: 'a',
        deps: ['b'],
        compute: ({ b, c }) => { // <-- 'c' shouldn't exist here
            return 2 * b + c.length; 
        },
    },
};

我预计推断的类型为:

(property) compute: (deps: {
    b: number;
}) => number

然而,它是:

(property) compute: (deps: {
    a: number;
    b: number;
    c: string;
}) => number

如何让TypeScript按照我想要的方式推断类型?

推荐答案

Graph类型在其NodeDef<N, D>个属性中使用Node作为D类型参数,因此Graph的每个属性显然都依赖于所有 node .如果这不是您想要的,那么您需要想出一种方法来为D类型参数插入更具体的内容.可以这样写Graph,这样每个属性都是every possible DNodeDef<K, D>union,但是这个并集会随着Node的成员数量呈指数级增长,这很快就会变得不可用.所以我们不要试着这么做.

因此,您会希望Graph本身为generic并接受一个类型参数,该参数表示每个属性的D种类型.因此,它应该是Graph<D>,对于keyof D中的每个K,属性的类型将是NodeDef<K, D[K]>.也许是这样的:

type Graph<D extends Record<keyof D, Node>> = {
  [K in Node & keyof D]: NodeDef<K, D[K]>
};

在这里,D具有不在Node中的密钥在技术上是可能的,但是由于具有Nodekeyof Dintersection,这些密钥将被从结果中排除.


现在您可以为您的类型命名,但它需要手动annotation,这会导致您同时使用值和类型进行重复:

const config: Graph<{ a: "b" }> = {
  a: {
    name: 'a',
    deps: ['b'],
    compute: ({ b, c }) => { // error
      // --------> ~
      // Property 'c' does not exist on type '{ b: number; }'.
      return 2 * b + c.length;
    },
  },
};

这完全符合你的要求,但你必须自己写{a: "b"}. 如果TypeScript可以推断泛型类型中的类型参数,那就太好了,但它不能. 但是,当你调用泛型时,它会推断类型参数. 因此,你可以提供一个名为graph()的帮助函数,它为你推断D,并用函数调用替换注释:

const config = graph({
  a: {
    name: 'a',
    deps: ['b'],
    compute: ({ b, c }) => { // error
      // --------> ~
      // Property 'c' does not exist on type '{ b: number; }'
      return 2 * b + c.length;
    },
  },
});
config;
// ^? const config: Graph<{ a: "b" }>

所以现在config被推断为类型Graph<{a: "b"}仅仅是因为graph()的参数. 我们现在要做的就是定义graph().


这里有一种方法:

const graph = <const D extends Record<keyof D, Node>>(
  g: { [K in keyof D]: K extends Node ? NodeDef<K, D[K]> : never }
): Graph<D> => g;

TypeScrip的泛型推理有点棘手;可以从mapped type中推断出D,而不是D的键(即,如果它是homomorphic映射的类型,请参阅What does "homomorphic mapped type" mean?以了解更多信息).所以它必须是in keyof D,而不是in Node & keyof D.因此,我将"AND Node"限制移至属性类型,将其设置为conditional type K extends Node? NodeDef<K, D[K]> : never.keyof D中没有出现在Node中的任何键都将被映射到never,您将得到一个错误.

Playground link to code

Typescript相关问答推荐

如何在类型脚本中声明对象其键名称不同但类型相同?

如何使用axios在前端显示错误体

TypScript手册中的never[]参数类型是什么意思?

使用泛型keyof索引类型以在if-condition内类型推断

角效用函数的类型推断

迭代通过具有泛型值的映射类型记录

判断映射类型内的键值的条件类型

我可以使用typeof来强制执行某些字符串吗

创建依赖函数参数的类型

为什么TypeScrip不能解析对象析构中的赋值?

如何编辑切片器以使用CRUD调用函数?

回调函数中的TypeScrip类型保护返回Incorect类型保护(具有其他未定义类型的返回保护类型)

抽象类对派生类强制不同的构造函数签名

我不明白使用打字脚本在模板中展开参考

Angular NG8001错误.组件只能在一个页面上工作,而不是在她的页面上工作

为什么类型脚本使用接口来声明函数?他的目的是什么.

替换typescript错误;X类型的表达式可以';t用于索引类型Y;带有未定义

什么';是并类型A|B的点,其中B是A的子类?

TS 排除带点符号的键

TS2532:对象可能未定义