【问题标题】:Use inferred generic param type of one function to type other functions使用一个函数的推断泛型参数类型来键入其他函数
【发布时间】:2020-01-24 19:12:09
【问题描述】:

我正在构建一个 API 来呈现架构图。

这是它的样子(超简化)

interface Node {
  name: string;
}

type NodeNames<T extends Node[]> = T[number]["name"]; // All node names as union of string literal

type Scalars = 'Int' | 'Boolean' | 'String'

type AllTypes<T extends Node[]> = NodeNames<T> | Scalars

interface Schema<T extends Node[]> {
  createNode(name: String, fields: Record<string, AllTypes<T>>): Node;

  render(types: T[]): string;
}

const s: Schema = { // generic not captured => "TypeError: Generic type 'Schema<T>' requires 1 type"

  render(types) {
    // print types
    return ''
  },
  createNode(name, fields) {
    return { name, fields };
  }
};

const Blog = s.createNode("Blog", { users: "User" });
const User = s.createNode("User", { posts: "Post" });
const Post = s.createNode("Post", { title: "String", comments: 'Comment' }); // <== Expected type error because 'Comment' node doesn't exist

s.render([Blog, User, Post]);

我想确保不能从 Schema.createNode 函数引用未在 Schema.render 中注册的类型。

在上面的示例中,fields 基本上应该是以下类型:Record&lt;string, Scalars | 'User' | 'Post' | 'Blog',其中User | Post | Blog 是从传递给s.render 的节点推断出来的

为此,我想推断传递给render 函数的节点的名称,以便输入Schema.createNode 的参数fields 的值。

不幸的是,泛型只有在函数级别声明时才会被函数参数捕获,而在接口级别声明时不会被捕获(这是有道理的)。

如何重用 s.render&lt;T extends Node[]&gt;(nodes: T) 的推断泛型来键入函数 createNode(name: string, fields: AllTypes&lt;T&gt;),其中 T 是两个函数之间共享的相同推断类型?

有没有办法让它工作? (即使 API 略有不同)

谢谢????

【问题讨论】:

  • 您创建了const s 类型的Schema,而不是给它一个它需要的类型参数。它不会以Schema&lt;Any&gt; 之类的东西结束,所以createNode 对其fields 参数没有任何类型限制?
  • 没有,上面的代码有类型错误。正如主题中所说,接口泛型不是由一个接口中的函数参数捕获的,这是有道理的,否则一个接口的任何函数都可能改变其泛型类型

标签: typescript typescript-typings typescript-generics


【解决方案1】:

我认为你不能让它与多个调用一起工作。最好的解决方案是创建一个将模式定义作为参数的类并检查整个对象:

interface Node<T extends string = string> {
    name: T;
}


type Scalars = 'Int' | 'Boolean' | 'String'

type AllTypes<T extends Record<keyof T, any>> = keyof T | Scalars
type Values<T> = T[keyof T]

type SchemNodes<T extends Record<keyof T, Record<string, AllTypes<T>>>> =
    { [P in Extract<keyof T, string>]: Node<P> }

class Schema<T extends Record<keyof T, Record<string, AllTypes<T>>>> {
    constructor(types: T) { }
    getNodes(): SchemNodes<T> {
        return null!;
    }
    renderTypes(types: Array<Values<SchemNodes<T>>>) { }
}

const s = new Schema({
    "User": { posts: "Post" },
    "Blog": { users: "User" },
    // "Comment": {},
    "Post": { title: "String", comments: 'Comment' }
})

const { Blog, Post, User } = s.getNodes();

Play

【讨论】:

  • 感谢您的回答提香?。虽然我确实说过“即使 API 略有不同”,但 API 必须允许跨多个文件拆分节点定义,因为可能定义了数千个类型/节点。仅供参考,这与 nexus.js.org 有关,这是一个 graphql 模式构建库,目前在运行时生成 ts 定义。我正在探索通过静态类型实现相同级别的类型安全的方法
  • @FlavianDesverne 是的,我明白你在说什么。我可能会再试一次。在调用方法时更改变量的类型是可能的(尤其是在具有自定义断言的下一个版本中)。最大的问题是你怎么知道User 何时注册Post 稍后会注册。除非您在注册完所有内容后最后检查,否则无法做到这一点。
  • TBH,我不认为这是可能的,来到 SO 是我最后一次尝试寻找 TS 向导。如果没有注册节点/类型,fields 应该只是类型Scalars。为了能够正确引用Post,您首先需要使用其所有标量字段创建它,然后将其传递给render 进行注册,然后从其他类型引用它(例如:User) .给定传递给render 的类型,Nexus 通过在运行时生成全局 ts 定义来实现这一点。使用接口试图通过跨多个函数共享单个泛型来删除这些生成的全局 ts 定义
  • @FlavianDesverne 您是否对调用s.createNode 但不返回节点的解决方案感兴趣。您必须改为调用 s.allNodes 之类的名称。在满足所有依赖项之前,s.allNodes 无效?如果您愿意,我们可以在 gitter 上聊天,也许会改进解决方案
  • 在 gitter 上 ping 你,很高兴随时聊天!
猜你喜欢
  • 1970-01-01
  • 2020-03-01
  • 2020-10-10
  • 2019-01-07
  • 2016-11-01
  • 2020-03-12
  • 1970-01-01
  • 2018-10-20
  • 2020-01-23
相关资源
最近更新 更多