如何指定只将相关属性分配给类实例?

时间:2018-05-01 17:47:01

标签: typescript generics

我有一个函数,它接受一个对象(DataObject)并使用它的一些属性来实例化一个类。

要知道应该将数据对象中的哪些项分配给类中的哪个属性,我将以元组列表的形式使用映射。元组中的第一项是数据对象中的键,第二项是类中的属性名称。

interface DataObject<T> {
  [name: string]: T[keyof T];
}

// list of mappings
type MappingsList<T> = [string, keyof T][];

// function that instantiates a class and assigns its properties
// from the data object using the mapping from `mappings`
function AttrConstructor<T>(
  ItemClass: { new (): T },
  mappings: MappingsList<T>,
  dataObj: DataObject<T>
) {
  const instance = new ItemClass();
  mappings.forEach(([fromLabel, toLabel]) => {
    instance[toLabel] = dataObj[fromLabel];
  });

  return instance;
}

这本身很有效,但是当我指定dataObject包含多个类的属性和值时会出现问题。

class Class1 {
  Prop1a: string;
  Prop1b: number;
}
class Class2 {
  Prop2a: string;
  Prop2b: number;
}

declare const row: DataObject<Class1 & Class2>;

const mappings1: MappingsList<Class1> = [["prop1a", "Prop1a"]];

const makeNew1 = (row: DataObject<Class1>) =>
  AttrConstructor(Class1, mappings1, row);

const instance1 = makeNew1(row);

然后我收到此错误:

Argument of type 'DataObject<Class1 & Class2>' is not assignable to parameter of type 'DataObject<Class1>'.
  Type 'Class1' is not assignable to type 'Class1 & Class2'.
    Type 'Class1' is not assignable to type 'Class2'.
      Property 'Prop2a' is missing in type 'Class1'.

我的问题是,如果dataObject包含的属性多于当前类所需的属性,我该如何指定它是正常的,因为AttrConstructor函数将确保只有与...相关的属性每个班级都被分配了?

PS :当我从真实代码中汇总这个代码示例时,我实际上没有任何错误,直到我保存文件,所以也许我的tsconfig.json可能与此相关:

// tsconfig.json
{
  "include": ["src/**/*"],
  "compilerOptions": {
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "module": "commonjs",
    "outDir": "dist",
    "pretty": true,
    "lib": ["es2015"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

1 个答案:

答案 0 :(得分:1)

你的直觉和我的直觉是DataObject<Class1>DataObject<Class2>在结构上应与{[k: string]: string | number}相同。它们是,但TypeScript显然无法识别前者与后者相同。我不是百分之百确定为什么会这样,但我的猜测是,在确定这两种类型不兼容之前,TypeScript没有足够深入地分析DataObject<T>接口。由于DataObject<T>取决于属性值位置中的Tkeyof T,因此可能已决定,T 中的类型应为不变量,这意味着DataObject<T>DataObject<U>仅在TU相同时才兼容。通常这是合理的行为,但是DataObject<T>故意抛弃它对T的一些依赖,因此TypeScript在这里错过了结构等价。 (你可能想file a GitHub issue关于这一点,除非那里已经有一个......但还没有找到相关的那个。)

解决此问题的一种方法是将interface DataObject<T> {...}更改为type DataObject<T> = {...}。这是因为类型别名不是新类型,而是现有类型的名称。因此,当面对DataObject<Class1 & Class2>DataObject<Class1>时,编译器会在开始时将它们中的每一个减少到{[k: string]: string | number},因此您的代码将按预期进行编译。

另一种解决方法是将定义更改为interface DataObject<P> {[k: string]: P},定义type ValueOf<T> = T[keyof T],然后使用DataObject<ValueOf<Class1>>DataObject<ValueOf<Class1 & Class2>>代替DataObject<Class1> }和DataObject<Class1 & Class2>。编译器对DataObject<P>取决于P的方式的启发式现在将是准确的。但是,您可能不喜欢这样,因为它会强制您改变使用DataObject<T>的方式。

如果您需要使用现有界面,可能还有其他解决方法。一种可能性是创建一个函数,如果它们在结构上相容,则将DataObject<T>转换为DataObject<U>。类似的东西:

type Id<T> = {[K in keyof T]: T[K]}
function cast<T, U>(x: U & (Id<U> extends Id<T> ? U : never)): T {
  return x as any as T;
}
const instance1 = makeNew1(cast(row)); // works, maybe?

依赖于conditional types,这是TypeScript v2.8中引入的一项功能。只要cast()以结构方式扩展TU就会从U转换为T

好的,这就是我现在所能想到的。希望能帮助到你。祝你好运!