简介
在我们的项目中,我们有属性支持,其中每个属性都是类。该属性包含有关类型、可选性和名称的信息。我不想为每个实体定义一个接口,而是想将其自动化。我们有大约 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
}
IDE 显示的内容
使用IntelliJ IDEA Ultimate。
// notice double equals
const mapped: DataType<typeof SomeClass.attributes> == {
test: string | number
test2: number | number
}
我已经花了 5 个小时来解决它。看来我错过了一些重要的东西。我要感谢所有给我任何提示的人,我做错了什么。
有两个问题:
- 一切都是必需的(test2应该是可选的)
- 即使我推断它们,类型也是混合的
TypeScript Playground 的链接
我将回答这个问题的精简版本,该问题忽略了特定的类定义以及具有静态属性和实例的构造函数之间的差异。 您可以在完整版本中使用下面介绍的一般技术并进行适当的转换。
给定以下接口,
interface AttributeInterface {
attrName: string;
required?: boolean;
valueType: StringConstructor | NumberConstructor | BooleanConstructor;
}
我将提出一个将T
(AttributeInterface
的联合)转换为它所代表的实体的DataType<T extends AttributeInterface>
。 请注意,如果你有一个数组类型Arr
像[Att1, Att2]
你可以通过查找它的number
索引签名来将其转换为联合: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
并将其分为两部分:必需属性Extract<T, { required: true }>
,以及不需要的属性Exclude<T, { required: true }>
,其中Extract
和Exclude
是过滤联合的实用程序类型。
对这两个部分所做的处理之间的唯一区别是,前者的映射类型是必需的(定义中没有?
),而后者的映射类型是可选的(定义中带有?
),根据需要...然后我把它们交叉在一起。
无论如何,对于这些T
的attrName
属性中的每个键K
,属性值都是类型ReturnType<Extract<T, { attrName: K }>["valueType"]>
。Extract<T, {attrName: K}>
只是找到了T
中一个以K
为attrName
的成员。 然后我们查找它的"valueType"
属性,我们知道它是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
这意味着我们可以通过使用ReturnType
实用程序类型轻松获取基元类型。
唯一要解释的就是... extends infer O ? { [K in keyof O]: O[K] } : never
. 这是一个技巧,可以将像{foo: string} & {bar?: number}
这样的交集类型转换为像{foo: string; bar?: number}
这样的单个对象类型。
同样,将其转换为采用数组类型的形式很简单:
type DataTypeFromArray<T extends AttributeInterface[]> = DataType<T[number]>;
type AlsoEntity = DataTypeFromArray<[AttrTest, Attr2Test]>;
/* type AlsoEntity = {
test: string;
test2?: number | undefined;
} */
这应该有助于为示例代码中的类构建解决方案。
好的,希望有帮助;祝你好运!
操场链接到代码
我还有其他但有效的解决方案...
// 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,
};
操场