我想将“元数据”嵌入到类型中,以用于创建类型安全的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">>;
预期:名称===“用户”
实际:名称===“用户” | “帖子”
答案 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
对我很好。希望能有所帮助;祝你好运!