网站搜索

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 链接