typescript - 如何在 TypeScript 中强制将一个字段的类型与泛型类型中的另一个字段的类型相同?
假设我有那段代码:
type MyStr = "one" | "two" | "three";
const x: MyType<MyStr> = { master: "one", slave: "one" }; // should compile
const y: MyType<MyStr> = { master: "one", slave: "two" }; // should not compile
我的目标是创建一个类型 MyType,使从属者必须与主控者具有相同的类型。
一个解决方法是这样做:
type MyType<T extends string> = {
[K in T]: { master: K, slave: K }
}[T];
这个想法是创造所有可能性,拥有所有版本:
type MyType<T extends string> = {
[K in T]: { master: K, slave: K }
};
// MyType<"one" | "two" | "three"> = {
// "one": { master: "one", slave: "one" },
// "two": { master: "two", slave: "two" },
// "three": { master: "three", slave: "three" },
// }
并且在 T 的索引处取该类型就得到我们选择的类型。
这种方法可行,但当字段很多时,TypeScript 服务器会超负荷并崩溃。
有没有更好的方法来实现这一点?
当然,这只是问题的演示,而我的真实问题是要创建类似这样的东西:
type ElementFieldType<T, K extends keyof T> = {
name: K;
Element: FC<{ value: T[K], onChange: (value: T[K]) => void }>;
}
所以我需要 K 在 name 和 Element 的值中被强制为相同的类型,这意味着一旦 name 被设置为某个键,Element 就被强制为处理该键的类型
答案#1
你的 MyType<T> 会在 T 的联合类型上 分发,所以 MyType<T1 | T2 | T3> 等价于 MyType<T1> | MyType<T2> | MyType<T3>。但显然你有一些用例,当 T 中的联合类型太大时,这种方式对你不起作用。MyType<T> 的特定形式是微软在 microsoft/TypeScript#47109 中提出的 分发对象类型。如果你愿意,你可以将其改为分发条件类型:
type MyType<T> = T extends unknown ? { master: T, slave: T } : never;
const x: MyType<MyStr> = { master: "one", slave: "one" }; // okay
const y: MyType<MyStr> = { master: "one", slave: "two" }; // error
但是如果分配对象类型对编译器来说太多了,就不明显分配条件类型是否能提供足够的帮助。
如果你无法预先计算给定 T 的所有可能性,那么你将需要添加一个通用类型参数,以便只计算相关的可能性:
type MyType<T, U extends T> = { master: U, slave: U };
这里 T 对应你的原始联合类型,而 U 是你想支持的成员。这让你可以写
const x: MyType<MyStr, "one"> = { master: "one", slave: "one" }; // okay
const y: MyType<MyStr, "one"> = { master: "one", slave: "two" }; // error
但是现在你在类型里面写了“one”。
如果你能通过写类似 const x: MyType<MyStr, infer>={⋯} 或甚至只是 const x: MyType<MyStr>={⋯} 的方式让编译器为你 推断 那该多好,但它做不到。微软的 TypeScript 仓库里有一个相关的功能请求 microsoft/TypeScript#32794,不过目前这是不可能的,你需要想办法绕过它。目前泛型类型参数只有在调用泛型 函数 时才会被推断,所以这里通常的解决方案是写一个辅助函数,它只是返回输入值,但能给你想要的推断。
但是即使对于通用函数,你也不能手动指定一个类型参数,比如 MyType,然后让编译器推断另一个类型参数。要么全部指定,要么全部由编译器推断。TypeScript 需要像 microsoft/TypeScript#26242 中请求的那样 部分类型参数推断,但是目前仍然不可行,你需要绕过它。这里通常的解决方法是将函数柯里化,所以不是 <T, U extends T>(m: MyType<T, U>) => MyType<T, U>,而是 <T>() => <U extends T>(m: MyType<T, U>) => MyType<T, U>:
const myType = <T,>() => <U extends T>(m: MyType<T, U>) => m;
然后你可以指定 T 并将结果保存到一个新函数中:
const myTypeMyStr = myType<MyStr>();
并使用它:
const x = myTypeMyStr({ master: "one", slave: "one" }); // okay
const y = myTypeMyStr({ master: "one", slave: "two" }); // no error!! 💥
等等,它没有成功。为什么不行?
问题在于 TypeScript 非常乐意为 myTypeMyStr 内的 T 推断出联合类型 "one" | "two",因此也会在 myType 内推断该类型。避免这种情况的方法是告诉编译器,你可以从 master 推断 T,但不能从 slave 推断。做到这一点最简单的方法是使用 NoInfer 工具类型:
type MyType<T, U extends T> = { master: U, slave: NoInfer<U> };
现在其他所有的功能都按预期工作了:
const myType = <T,>() => <U extends T>(m: MyType<T, U>) => m;
const myTypeMyStr = myType<MyStr>();
const x = myTypeMyStr({ master: "one", slave: "one" }); // okay
const y = myTypeMyStr({ master: "one", slave: "two" }); // error
所以就这样吧。它能运行,但不幸的是,要实现无缝支持所需的功能尚未实现,而且解决方法有些笨拙。但现在这是你能做的最好的了。
代码的 Playground 链接
