我能想到的最简单的例子:
interface text {
type: "text";
text: string;
}
interface integer {
type: "integer";
integer: number;
}
type Config = text | integer;
function appendToBody(config: Config) {
if (!config[config.type]) return;
document.body.append(config[config.type]);
}
function createConfig(type: "text" | "integer", value: string | number) {
// how to let TS know that type and [type] will be matching?
// TS naturally assumes { type: 'text', text: 5 } is possible, even though it isn't
const config: Config = {
type,
[type]: value
};
appendToBody(config);
}
createConfig("text", "hello world");
https://codesandbox.io/s/6gtq8
基本上,我使用的是通过要求obj [obj.type]提取值的模式。 这对我的实际情况很有用,因为我可以制作一个通用的解析器,根据类型提取所需的值。它的另一个优点是,如果类型更改,则不必清空,因为它将保存在其他[type]上,并且如果更改回来,您也不会丢失旧值。
我只是想不通如何使打字稿理解类型和[type]的所有可能组合都由“ Config”类型覆盖。
答案 0 :(得分:1)
首先,让我们清楚一点,integer
应该有一个名为integer
的密钥,而不是number
,对吧?像这样:
interface text {
type: "text";
text: string;
}
interface integer {
type: "integer";
integer: number; // right?
}
type Config = text | integer;
function appendToBody(config: Config) {
if (!config[config.type]) return;
document.body.append(config[config.type]);
}
好的。
createConfig()
这里有两个类型安全问题...一个是为调用方强制执行类型安全,另一个是在实现中强制执行类型安全 em>。现在,编译器在实现内部警告您,它无法验证{ type: type, [type]: value }
是有效的Config
。现在,警告您是正确的,因为呼叫者可以执行以下操作而不会出错:
createConfig("text", 5); // no error, oops!
没有简单的方法可以同时为调用者和实现者修复此问题。双方都有自己的问题。
要为呼叫者解决此问题,可以使用overloads,如下所示:
function createConfig(type: "text", value: string): void;
function createConfig(type: "integer", value: number): void;
function createConfig(type: string, value: any): void {
// impl
}
这很容易理解,但是需要为Config
联合的每个组成部分添加一个重载。您也可以使用conditional类型和generic函数,如下所示:
type Lookup<T, K> = K extends keyof T ? T[K] : never;
type ConfigFor<T extends Config['type']> = Extract<Config, { type: T }>;
function createConfig<T extends Config['type']>(
type: T,
value: Lookup<ConfigFor<T>, T>
) {
// impl
}
这很复杂,但是假设C
联合中的每个元素Config
都符合type
属性为value属性命名相关键的约束,就会自动正确地运行。
以上任何一种都会导致呼叫者出现以下行为:
createConfig("text", 5); // error
createConfig("integer", 5); // okay
createConfig("text", "hello world"); // okay
createConfig("integer", "hello world"); // error
要针对实现进行修复(这是您的实际问题),即使使用固定的调用签名,编译器仍将无法确定config
是有效的Config
。对于重载,这是因为实现签名太松散而无法表达约束,并且是重载实现do not currently do any control flow analysis based on the call signatures。对于通用条件类型,这是因为条件类型还依赖于其中的未解析的通用类型参数do not get narrowed via control flow analysis。因此,在这两种情况下,编译器基本上都放弃了在实现内部强制执行相关数据类型的类型安全。我经常使用wished寻找某种机制,该机制将允许您提示编译器通过控制流分析来遍历联合类型,但这只是现在的幻想。
那么,你能做什么?据我所知,这里实际上只有两种方法。您可以使用type assertion:将代码保持原样,而只是告诉编译器您将负责确保类型安全,如:
const configAsserted = {
type,
[type]: value
} as any as Config;
appendToBody(configAsserted);
或者,您在运行时进行了额外的手动检查,以使编译器确信所执行的操作是安全的,例如:
let configManual: Config;
if (type === "integer" && typeof value === "number") {
configManual = { type: "integer", integer: value };
} else if (type === "text" && typeof value === "string") {
configManual = { type: "text", text: value };
} else {
throw new Error("YOU MESSED UP");
}
appendToBody(configManual);
两种方法都有效。断言的安全性较差,但在将成分添加到Config
时可以更好地扩展。手动检查是安全的,但它是多余的,并且每次向Config
添加成分时都必须添加代码。
所以我看到了您的选择。我个人会选择更好的扩展解决方案,例如:
type Lookup<T, K> = K extends keyof T ? T[K] : never;
type ConfigFor<T extends Config['type']> = Extract<Config, { type: T }>;
function createConfig<T extends Config['type']>(
type: T,
value: Lookup<ConfigFor<T>, T>
) {
const config = {
type,
[type]: value
} as any as Config;
appendToBody(config);
}
好的,希望能有所帮助。祝你好运!
答案 1 :(得分:0)
让您的type Config
灵活一些,强制它接受{type: string, [anything]: anything}
格式的任何类型。
type Config = text | integer | {type: string, [key: string]: any};