打字稿推断联合类型而不是指定类型
Typescript inferring union type instead of specified type
我想将 "metadata" 嵌入到一个类型中以用于创建类型安全的 REST 客户端。这个想法是使用 link 中的类型元数据来推断在 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]>;
除最后和关键位外,我能够完成大部分工作,从 link 类型推断路由名称:
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">>;
预计:姓名==="users"
实际:姓名==="users" | "posts"
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>
,从中无法确定超过 [=26= 中 R
的一般约束]定义:即keyof Schema
、a.k.a。 "users" | "posts"
。 TypeScript FAQ 有一个 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 };
幻数属性是可选的,所以你可以写
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
我觉得不错。希望有所帮助;祝你好运!
我想将 "metadata" 嵌入到一个类型中以用于创建类型安全的 REST 客户端。这个想法是使用 link 中的类型元数据来推断在 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]>;
除最后和关键位外,我能够完成大部分工作,从 link 类型推断路由名称:
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">>;
预计:姓名==="users"
实际:姓名==="users" | "posts"
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>
,从中无法确定超过 [=26= 中 R
的一般约束]定义:即keyof Schema
、a.k.a。 "users" | "posts"
。 TypeScript FAQ 有一个 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 };
幻数属性是可选的,所以你可以写
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
我觉得不错。希望有所帮助;祝你好运!