Typescript推断联合类型而不是指定类型

问题描述 投票:0回答:1

我想将“元数据”嵌入到类型中,以用于创建类型安全的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">>;

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

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

typescript type-inference conditional-types
1个回答
0
投票

TypeScript的类型系统是structural,不是标称值,意味着它是确定其身份的类型的shape,而不是类型的name。类型别名,如

type Link<R extends keyof Schema> = string

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

无论如何,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并向其中添加属性。

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

branded primitives

phantom属性是可选的,因此您可以编写

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

没有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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.