我有一个数据结构,其中一个键允许一组动态值。我知道这些值的潜在类型,但我无法在 Typescript 中表达。
interface KnownDynamicType {
propA: boolean;
}
interface OtherKnownDynamicType {
propB: number;
}
// I want to allow dynamic to accept either KnownDynamicType, OtherKnownDynamicType or a string as a value
interface DataModel {
name: string;
dynamic: {[key: string]: KnownDynamicType | OtherKnownDynamicType | string};
}
const data: DataModel = { // Set up some values
name: 'My Data Model',
dynamic: {
someKnownType: {
propA: true
},
someOtherKnownType: {
propB: 1
},
someField1: 'foo',
someField2: 'bar'
}
}
data.dynamic.foo = 'bar'; // Fine
data.dynamic.someObject = { // <--- This should be invalid
propA: false,
propB: ''
}
Run Code Online (Sandbox Code Playgroud)
打字稿似乎看到data.dynamic.someObject了KnownDynamicType但出乎意料地允许我为其分配属性OtherKnownDynamicType。
我已尽力避免复杂的打字,但是当出现情况时,我宁愿避免认输而只是设置 dynamic: any
我的问题是,如何在 Typescript 中正确表达上述内容?
联合确实是包含性的,因此A | B包括任何有效的A,以及任何有效的B,包括任何有效的A & B。TypeScript 没有否定类型,因此您不能说类似(A | B) & !(A & B),这会明确排除与A和都匹配的任何内容B。那好吧。
TypeScript 也缺少确切的类型,因此将属性添加到 validA仍然会产生有效的A. 因此,您不能说Exact<A> | Exact<B>, 这也将排除与A和匹配的任何内容B,以及具有除显式声明的Aor 或属性以外的任何属性的任何内容B。那好吧。
TypeScript确实有多余的属性检查,它通过禁止“新鲜”对象文字上的额外属性来提供否定或精确类型的一些好处。不幸的是,(从 TypeScript v2.8 v3.5 开始)存在一个问题,即对联合类型的额外属性检查并不像大多数人希望的那样严格,正如您所发现的。看起来这个问题被认为是一个错误,应该在未来的TypeScript v3.0 (edit:) 中解决,但这并不能解决你今天的问题。那好吧。
所以,也许我们可以喜欢我们的类型来获得类似于你想要的行为的东西:
type ProhibitKeys<K extends keyof any> = { [P in K]?: never }
type Xor<T, U> = (T & ProhibitKeys<Exclude<keyof U, keyof T>>) |
(U & ProhibitKeys<Exclude<keyof T, keyof U>>);
Run Code Online (Sandbox Code Playgroud)
让我们来研究一下。 ProhibitKeys<K>返回一个包含所有可选属性的类型,其键是 inK并且其值是 type never。所以ProhibitKeys<"foo" | "bar">相当于{foo?: never, bar?: never}. 由于never是不可能的类型,因此真实对象匹配的唯一方法ProhibitKeys<K>是它没有任何键在K. 由此得名。
现在让我们来看看Xor<T,U>。该类型Exclude<keyof A, keyof B>是所有声明的键的联合,其中A在B. Xor<T,U>是两种类型的联合。第一个是T & ProhibitKeys<Exclude<keyof U, keyof T>>,这意味着它是有效的T,没有来自的属性U(除非这些属性也在 中T)。第二个是U & ProhibitKeys<Exclude<keyof T, keyof U>>,这意味着U没有来自T. 这与在 TypeScript 中表达您想要的排他联合的想法非常接近。让我们使用它,看看它是否有效。
首先,改变DataModel:
interface DataModel {
name: string;
dynamic: { [key: string]: Xor<KnownDynamicType, OtherKnownDynamicType> | string };
}
Run Code Online (Sandbox Code Playgroud)
让我们看看会发生什么:
declare const data: DataModel;
data.dynamic.foo = 'bar'; // okay
data.dynamic.someObject = {
propA: false,
propB: 0
}; // error! propB is incompatible; number is not undefined.
data.dynamic.someObject = {
propA: false
}; // okay now
data.dynamic.someObject = {
propB: 0
}; // okay now
data.dynamic.someObject = {
propA: false,
propC: 0 // error! unknown property
}
Run Code Online (Sandbox Code Playgroud)
这一切都按预期工作。希望有帮助。祝你好运!