假设我有该代码:
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 处采用该类型给出我们选择的类型。
这种方法有效,但是当有很多字段时,打字稿服务器就会过载并死掉。
有更好的选择来实现这一目标吗?
当然,这只是问题的演示,我真正的问题是创建这样的东西:
type ElementFieldType<T, K extends keyof T> = {
name: K;
Element: FC<{ value: T[K], onChange: (value: T[K]) => void }>;
}
所以我需要 K 被强制为
name
中的类型和 Element
的值相同,这意味着一旦 name
被设置为某个键,Element
就被迫成为处理该键的类型钥匙
您的
MyType<T>
在 T
中的 unions上分配,因此
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> = {⋯}
的内容来为你推断,那就太好了,但它不能。 microsoft/TypeScript#32794 有一个开放的功能请求,但目前这是不可能的,您需要解决它。目前,只有在调用泛型函数时才会推断泛型类型参数,因此通常的解决方法是创建一个辅助函数,该函数仅返回其输入,但它会为您提供所需的推断。 但即使对于泛型函数,您也不能手动指定一种类型参数(如
MyType
)并让编译器推断出不同的类型参数。要么全有,要么全无。 TypeScript 需要按照
microsoft/TypeScript#26242中的要求进行部分类型参数推断,但同样,目前这是不可能的,您需要解决它。这里通常的解决方法是对函数进行 curry,这样你就可以得到
<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 非常乐意推断
"one" | "two"
内部
T
的并集 myTypeMyStr
,因此也推断在 myType
内部。避免 that的方法是告诉编译器您可以从
T
推断出 master
,但不能从 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