推断联合体类型而不是指定类型的打字稿

时间:2019-11-04 15:51:35

标签: typescript type-inference conditional-types

我想将“元数据”嵌入到类型中,以用于创建类型安全的REST客户端。想法是使用链接中的类型元数据来推断用于API调用的正确端点模式。例如。

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

type User = {
  self: Link<"users">;
};

const user: User = { self: "https://..." };

http(user.self, "GET", { userId: 1 });

我能够使用强力条件类型进行此操作。

例如

type Routes = "users" | "posts";
type Verbs<R> = R extends "users" ? "GET" : never;
type Query<R, V> = R extends "users"
  ? V extends "GET"
    ? { queryId: string }
    : never
  : never;

但是,这导致归一化类型模型难以手动输入。相反,我想使用非规范化的类型,例如。

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

使用如下类型:

type Query<
  S,
  RN extends keyof S,
  VN extends keyof S[RN]
> = OpQuery<S[RN][VN]>;

除了最后一位和关键位,我能够完成大部分工作,并从链接类型推断出路由名称:

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

type Link<R extends keyof Schema> = string;

type LinkRouteName<L> = L extends Link<infer R> ? R : never;

type name = LinkRouteName<Link<"users">>;

预期:名称===“用户”

实际:名称===“用户” | “帖子”

1 个答案:

答案 0 :(得分:1)

TypeScript的类型系统是structural而不是标称类型,这意味着它是确定其身份的类型的 shape ,而不是该类型的 name

之类的类型别名
type Link<R extends keyof Schema> = string

没有以任何方式定义依赖于R的类型。 Link<"users">Link<"posts">均得出string;它们只是同一类型的不同名称,因此不会对类型系统造成影响。从理论上讲,这两种类型是无法区分的……在某些情况下,编译器可以区分两个形状相同的类型,例如不同的名称,但您永远不要依赖于此。

无论如何,R类型的信息将被抛出,并且以下内容无法将其带回:

type LinkRouteName<L> = L extends Link<infer R> ? R : never;

LinkRouteName<Link<"users">>LinkRouteName<Link<"posts">>都被评估为LinkRoutName<string>,通过R定义中对Link<R>的一般约束,再也无法确定:即keyof Schema,也称为"users" | "posts"。 TypeScript常见问题解答中有一个similar example,其中类型推断无法带回丢弃的类型信息。


因此,如果要对两种类型进行不同的处理,则它们应具有不同的结构。如果Link<R>是对象类型,我建议向该对象添加一个名为name的属性,其值类型为R

但是您只使用原始的string类型。在运行时实际上不可能使原始类型在结构上有所不同(您不能像(var a = ""; a.prop = 0;那样向其添加属性)。您可以使用String wrapper type并向其中添加属性。

另一种可行的方法是通过使用称为“ branded primitives”的方法,误导编译器将原始string类型的值视为与string在结构上有所不同。您将原始类型与虚拟的“ brand”属性相交以用于区分类型。我的建议是:

type Link<S extends keyof Schema> = string & { __schema?: S };

phantom属性是可选的,因此将允许您编写

const userLink: Link<"users"> = "anyStringYouWant";

没有type assertion,但是您必须确保手动注释类型。以下内容无效:

const userLink = "anyStringYouWant";

那将是string,而不是Link<"users">


一旦有了,其余的就应该放到位。 http()函数的可能声明为:

declare function http<
  S extends keyof Schema,
  V extends keyof Schema[S],
  >(
    url: Link<S>,
    verb: V,
    ...[query]: Schema[S][V] extends { query: infer Q } ? [Q] : []
  ): void;

使用rest tuple types来表示http()是否可以采用第三个参数,这取决于相应的Schema条目是否具有相关的query属性。 / p>

让我们验证一下它是否有效:

type User = { self: Link<"users"> };
const user: User = { self: "https://..." };
http(user.self, "GET", { userId: "1" }); // okay
http(user.self, "GET", {}); // error!  userId missing
http(user.self, "GET"); // error! expected 3 arguments

type Post = { self: Link<"posts"> }
const post: Post = { self: "https://..." }
http(post.self, "POST"); // okay 
http(post.self, "POST", { userId: "1" }); // error! expected 2 arguments

对我很好。希望能有所帮助;祝你好运!

Link to code