在我们的项目中,我们具有属性支持,其中每个属性都是类。该属性包含有关类型,可选性和名称的信息。我不想为每个实体定义一个接口,而是要使它自动化。我们大约有500个属性和100 +-个实体。实体是属性的收集器。
interface AttributeClass {
readonly attrName: string;
readonly required?: boolean;
readonly valueType: StringConstructor | NumberConstructor | BooleanConstructor;
}
class AttrTest extends Attribute {
static readonly attrName = "test";
static readonly required = true;
static readonly valueType = String
}
class Attr2Test extends Attribute {
static readonly attrName = "test2";
static readonly valueType = Number
}
interface Entity {
test: string // AttrTest
test2?: number // Attr2Test
}
class SomeClass {
static attributes = [AttrTest, Attr2Test]
}
在这里您可以注意到,我有valueType
持有真实类型。我也知道这个名字,如果可以的话。 (需要required
是否存在并设置为true)
我的想法是遍历attributes
数组,将值映射到名称并使其可选。
export type ValueOf<T> = T[keyof T];
type FilterOptionalAttribute<Attr extends AttributeClass> = ValueOf<Attr["required"]> extends false | undefined | null ? Attr : never
type FilterRequiredAttribute<Attr extends AttributeClass> = FilterOptionalAttribute<Attr> extends never ? Attr : never
type ExtractPrimitiveType<A> =
A extends StringConstructor ? string :
A extends NumberConstructor ? number :
A extends BooleanConstructor ? boolean :
never
type AttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]: ExtractPrimitiveType<Attr["valueType"]> }
type OptionalAttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]?: ExtractPrimitiveType<Attr["valueType"]> }
type UnboxAttributes<AttrList> = AttrList extends Array<infer U> ? U : AttrList;
type DataType<AttributeList extends AttributeClass[]> = OptionalAttributeDataType<FilterOptionalAttribute<UnboxAttributes<AttributeList>>> & AttributeDataType<FilterRequiredAttribute<UnboxAttributes<AttributeList>>>
class SomeClass {
static attributes = [AttrTest, Attr2Test]
}
// notice double equals
const mapped: DataType<typeof SomeClass.attributes> == {
test: string
test2?: number
}
使用IntelliJ IDEA Ultimate
// notice double equals
const mapped: DataType<typeof SomeClass.attributes> == {
test: string | number
test2: number | number
}
我已经花了5个小时来解决它。似乎我缺少重要的东西。我要感谢所有给我任何提示的人,我在做什么错了。
有两个问题:
答案 0 :(得分:1)
我还有其他但可行的解决方案...
// Declare constructor type
type Constructor<T> = new (...args: any[]) => T;
// Declare support attribute types
type SupportTypes = [String, Number, Boolean];
// Attribyte class
class AttributeClass<K extends string, T extends SupportTypes[number], R extends boolean = false> {
constructor(
readonly attrName: K,
readonly valueType: Constructor<T>,
readonly required?: R,
) {
}
}
// Declare test attributes
const AttrTest = new AttributeClass('test', String, true);
const Attr2Test = new AttributeClass('test2', Number);
const attributes = [AttrTest, Attr2Test];
// Unwrap instance of AttributeClass, to object
type UnwrapAttribute<T> = T extends AttributeClass<infer K, infer T, infer R> ? (
R extends true ? {
[key in K]: T;
} : {
[key in K]?: T;
}
) : never;
// Transform union to intersection
// Example: UnionToIntersection<{a: string} | {b: number}> => {a: string, b: number}
type UnionToIntersection<U> = ((U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never);
// Transform tuple to intersection
// Example: TupleToIntersection<[{a: string}, {b: number}]> => {a: string, b: number}
type TupleToIntersection<U extends Array<any>> = UnionToIntersection<U[number]>;
// Map array of attributes
type MapAttributes<ArrT extends Array<AttributeClass<any, any, any>>> = TupleToIntersection<{
[I in keyof ArrT]: UnwrapAttribute<ArrT[I]>;
}>;
// Result
const mapped: MapAttributes<typeof attributes> = {
test: '123',
test2: 123,
};
答案 1 :(得分:1)
我将回答该问题的简化版本,该版本忽略特定的类定义以及具有静态属性和实例的构造函数之间的差异。您可以在完整版本中使用下面介绍的常规技术,并进行适当的转换。
给出以下界面,
interface AttributeInterface {
attrName: string;
required?: boolean;
valueType: StringConstructor | NumberConstructor | BooleanConstructor;
}
我将展示一个DataType<T extends AttributeInterface>
,它将{{1} s的 union 的T
转换为它表示的实体。请注意,如果您的数组类型为AttributeInterface
,例如Arr
,则可以通过looking up它的[Att1, Att2]
索引签名将其转换为并集:number
是{{1 }}。
无论如何,这里是:
Arr[number]
在解释之前,让我们在以下两个界面上进行尝试:
Att1 | Att2
看起来不错。
所以,解释:我将属性type DataType<T extends AttributeInterface> = (
{ [K in Extract<T, { required: true }>["attrName"]]:
ReturnType<Extract<T, { attrName: K }>["valueType"]> } &
{ [K in Exclude<T, { required: true }>["attrName"]]?:
ReturnType<Extract<T, { attrName: K }>["valueType"]> }
) extends infer O ? { [K in keyof O]: O[K] } : never;
的并集分为两部分:必需的属性interface AttrTest extends AttributeInterface {
attrName: "test";
required: true;
valueType: StringConstructor
}
interface Attr2Test extends AttributeInterface {
attrName: "test2";
valueType: NumberConstructor;
}
type Entity = DataType<AttrTest | Attr2Test>;
/* type Entity = {
test: string;
test2?: number | undefined;
} */
和不需要的属性T
,其中{{ 3}}和Extract
是用于过滤联合的实用程序类型。
对这两部分所做的处理之间唯一的区别是,前者是Exclude
的要求(定义中没有Extract<T, { required: true }>
),后者的映射类型是可选的(带有Exclude<T, { required: true }>
,然后……mapped type。
无论如何,对于那些?
的{{1}}属性的每个?
键,属性值的类型为K
。 attrName
仅找到T
作为其ReturnType<Extract<T, { attrName: K }>["valueType"]>
的{{1}}的一个成员。然后我们查找其Extract<T, {attrName: K}>
属性,我们知道它是T
,K
和attrName
中的一个(或多个)。
事实证明,这些类型中的每一个都是可调用的函数,该函数返回原始数据类型:
"valueType"
这意味着我们可以使用intersect实用程序类型轻松获得原始类型。
唯一需要说明的是StringConstructor
。采取NumberConstructor
之类的交集类型并将其转换为BooleanConstructor
之类的单个对象类型的技巧。
同样,将其转换为采用数组类型的形式很简单:
const s: string = String(); // StringConstructor's return type is string
const n: number = Number(); // NumberConstructor's return type is number
const b: boolean = Boolean(); // BooleanConstructor's return type is boolean
应该帮助您为示例代码中的类构建解决方案。
好的,希望能有所帮助;祝你好运!