我有一个函数,它接受一个对象(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
}
}
答案 0 :(得分:1)
你的直觉和我的直觉是DataObject<Class1>
和DataObject<Class2>
在结构上应与{[k: string]: string | number}
相同。它们是,但TypeScript显然无法识别前者与后者相同。我不是百分之百确定为什么会这样,但我的猜测是,在确定这两种类型不兼容之前,TypeScript没有足够深入地分析DataObject<T>
接口。由于DataObject<T>
取决于属性值位置中的T
和keyof T
,因此可能已决定,T
中的类型应为不变量,这意味着DataObject<T>
和DataObject<U>
仅在T
和U
相同时才兼容。通常这是合理的行为,但是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()
以结构方式扩展T
,U
就会从U
转换为T
。
好的,这就是我现在所能想到的。希望能帮助到你。祝你好运!