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