具有动态但必需的属性类型的TypeScript对象

时间:2018-08-23 15:09:03

标签: typescript

我有以下情况。我需要创建一个对象,该对象的属性可以是两种可能的类型(自定义类)之一。

export class MyType1 {
   // some properties
}

export class MyType2 {
   // some properties
}

class CustomType = MyType1 | MyType2

class Config {
   propOne: boolean;
   dynamicParam: { [key: string ]: CustomType }
}

Config 对象将按以下方式使用:

let config: Config = {
   propertyOne: true,
   dynamicParam: {
      key1: {
         // properties from one type
      },
      key2: {
         // properties from another type
      }
   }
}

如果在定义对象时指定属性的类型,如下所示:

   key1: <MyType1> {

   }

我对MyType1类的属性有理解。但是,如果我不指定类型,则对MyType1和MyType2两个类的属性都具有智能感知(因为dynamicParam属性的类型为CustomType [union type])。

我的问题是,是否可以在定义对象时进行所需的类型定义,以便在尝试将属性定义为:

   key1: {

   }

我收到必须指定类型的错误消息?

2 个答案:

答案 0 :(得分:3)

让我们修复您的代码,使其编译并具有一些特定的属性,以了解您的意思:

interface MyType1 {
  a: string,
  b: number,
  c: boolean
}

interface MyType2 {
  d: string,
  e: number,
  f: boolean
}

type CustomType = MyType1 | MyType2

我想也许您不满意下面的代码无怨无悔地编译:

let customType: CustomType = {
  a: "yes",
  b: 2,
  c: false,
  d: {what: "the"} // no error here
}

即使TypeScript中的类型不是"exact types"(其中值仅限于在类型定义中显式指定的属性),但是当您处理对象文字时,它们的行为更像是精确类型。当您将对象文字分配给变量时,文字会经受excess property checking的警告,如果文字包含任何意外的属性。例如,以下将是错误:

let warnCustomType: CustomType = {
  a: "yes",
  b: 2,
  c: false,
  nope: { what: "the" } // error
  // Object literal may only specify known properties, 
  // and 'nope' does not exist in type 'CustomType'.
}

但是,对于CustomType这样的联合类型(不是discriminated unions),多余的属性检查不适用于联合的每个组成部分;相反,它适用于整个工会。因此,不会警告您上面的d属性,因为dCustomType联合的至少一个组成部分的有效属性。这种现象被认为是bug,在以后的TypeScript版本中可能会得到解决。但是我不会屏住呼吸等待它。

您的建议以某种方式要求开发人员断言对象文字属性的类型很有趣,但我认为这是不可能的。您可以提出一个约定,但是该语言中的任何内容都不会强迫某人遵循该约定。

相反,您可能可以通过为所讨论的联合显式形成类似于确切类型的东西,然后要求该属性为该类型来摆脱困境。考虑使用conditional types的以下类型函数:

type AllPossibleKeysOf<U> = U extends any ? keyof U : never;

type Exclusify<U> = [U] extends [infer V] ? V extends any ? (
  V & Partial<Record<Exclude<AllPossibleKeysOf<U>, keyof V>, never>>
) : never : never

我不会过多地介绍它们的工作原理,但基本上Exclusify<U>将联合体U分解成其组成部分,并形成一个新的联合体,其中每个组成部分都明确禁止使用联合中的其他类型。

让我们看看它在CustomType上的作用:

type ExclusiveCustomType = Exclusify<CustomType>;

如果您检查它变为

(MyType1 & Partial<Record<"d" | "e" | "f", never>>) | 
(MyType2 & Partial<Record<"a" | "b" | "c", never>>)

大致相同
{a: string, b: number, c: boolean, d?: never, e?: never, f?: never} |
{a?: never, b?: never, c?: never, d: string, e: number, f: boolean}

类型为never的那些可选属性实际上是不可用的,或者完全不存在,或者undefined(如果存在)。现在,如果您尝试以下操作:

let customType: ExclusiveCustomType = {
  a: "yes",
  b: 2,
  c: false,
  d: {what: "the"} // error
}

d不是string | undefined时会收到错误消息(因为如果使用联合的第一部分,则d的类型为string或{ {1}}(如果使用联合的第二部分)。如果尝试通过将其设置为undefined来进行修复,则仍然会出现错误:

string

因此,let customType: ExclusiveCustomType = { a: "yes", b: 2, c: false, d: "nope" } // error! Types of property 'd' are incompatible. ExclusiveCustomType更具限制性。我不知道这是否是您想要的方式,但这是 a 方式。也许其他人会有不同的想法。希望能有所帮助。祝你好运!

答案 1 :(得分:0)

由于TypeScript具有结构类型系统,因此将接受具有MyType1MyType2所有属性的对象文字。我不知道有任何合理的方法来要求像<MyType1>这样的类型断言,但是如果您在类型定义中的某处添加了判别属性,则在配置中将需要它。例如,您可以这样做:

export enum MyTypes { 
    TYPE1, TYPE2
}

export class MyType1 {
    kind: MyTypes.TYPE1;
   // some properties
}

export class MyType2 {
    kind: MyTypes.TYPE2;
   // some properties
}

type CustomType = MyType1 | MyType2;

class Config {
   propertyOne: boolean;
   dynamicParam: { [key: string ]: CustomType }
}

let config: Config = {
    propertyOne: true,
    dynamicParam: {
        key1: {
            kind: MyTypes.TYPE2,
            // properties from one type
        },
        key2: {
            kind: MyTypes.TYPE1,
            // properties from another type
        }
    }
};