【问题标题】:Typescript inferring union type instead of specified type打字稿推断联合类型而不是指定类型
【发布时间】:2019-11-04 15:51:35
【问题描述】:

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

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

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

【问题讨论】:

  • type Link&lt;R extends keyof Schema&gt; = string;R 没有任何作用,所以我很困惑。您是否尝试获取 branded primitive 以便编译器记住该字符串以某种方式“意味着”"users" 调用而不是 "posts" 调用?
  • 喜欢this?
  • @jcalz 是的,这是正确的,尽管它不是关于品牌,而是更多关于类型元数据。 “R”对 Link 类型没有任何作用,它只是传达此 Link 指向特定路由的元数据。我们可以使用该类型元数据为该路由端点提供动词、查询参数和正文的类型安全列表。
  • @jcalz 看看你的例子,看来你得到了正确的推断。尝试映射回我的解决方案以确认这是有效的。
  • @jcalz 好的,我明白了。问题是 Typescript 将我的 Link 类型视为字符串,而不管我声明它的类型信息如何。但是通过“&”,类型系统现在尊重“R”具有一定意义的事实,并且类型不仅仅是一个字符串。好的!谢谢。

标签: typescript type-inference conditional-types


【解决方案1】:

TypeScript 的类型系统是structural 而不是名义上的,这意味着它是一个类型的shape 决定了它的身份,而不是类型的name。像

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

没有以任何方式定义依赖于R 的类型。 Link&lt;"users"&gt;Link&lt;"posts"&gt; 都计算为 string;它们只是同一类型的不同名称,因此不会对类型系统产生影响。从理论上讲,这两种类型彼此无法区分......在某些情况下,编译器可能区分两种形状相同的类型,例如不同的名称,但您永远不应该依赖它。

反正R类型的信息扔出去了,下面也带不回来了:

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

LinkRouteName&lt;Link&lt;"users"&gt;&gt;LinkRouteName&lt;Link&lt;"posts"&gt;&gt; 都被评估为 LinkRoutName&lt;string&gt;,在 Link&lt;R&gt; 定义中对 R 的一般约束之后无法确定更明确的定义:即 keyof Schema,a.k.a.@ 987654341@。 TypeScript FAQ 有一个similar example,其中类型推断无法恢复丢弃的类型信息。


因此,如果您想区别对待两种类型,它们应该具有不同的结构。如果Link&lt;R&gt; 是一个对象类型,我建议为该对象添加一个属性,例如name,其值为R

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

另一种方法是通过使用称为“branded primitives”的东西来误导编译器将原始string 类型值视为在结构上与string 不同。您将原始类型与用于区分类型的幻象“品牌”属性相交。我的建议是:

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

幻像属性是可选的,所以你可以写

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

没有type assertion,但您必须确保手动注释类型。以下将不起作用:

const userLink = "anyStringYouWant";

这只是string 而不是Link&lt;"users"&gt;


一旦你有了它,它的其余部分就应该到位。 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

我觉得不错。希望有帮助;祝你好运!

Link to code

【讨论】:

  • 工作就像一个魅力,感谢您提供的出色背景信息。
猜你喜欢
  • 2022-01-22
  • 2022-01-05
  • 1970-01-01
  • 2018-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-09
相关资源
最近更新 更多