使用带有联合类型签名的索引类型的意外行为

Rob*_*ula 1 types typescript

我有一个数据结构,其中一个键允许一组动态值。我知道这些值的潜在类型,但我无法在 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.someObjectKnownDynamicType但出乎意料地允许我为其分配属性OtherKnownDynamicType

我已尽力避免复杂的打字,但是当出现情况时,我宁愿避免认输而只是设置 dynamic: any

我的问题是,如何在 Typescript 中正确表达上述内容?

jca*_*alz 6

联合确实是包含性的,因此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>是所有声明的键的联合,其中AB. 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)

这一切都按预期工作。希望有帮助。祝你好运!