将数组映射到具有正确类型的对象(仅类型方式)

时间:2020-04-03 02:45:07

标签: typescript typescript-typings

简介

在我们的项目中,我们具有属性支持,其中每个属性都是类。该属性包含有关类型,可选性和名称的信息。我不想为每个实体定义一个接口,而是要使它自动化。我们大约有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数组,将值映射到名称并使其可选。

  1. 用于过滤可选属性的类型
export type ValueOf<T> = T[keyof T];
type FilterOptionalAttribute<Attr extends AttributeClass> = ValueOf<Attr["required"]> extends false | undefined | null ? Attr : never
  1. 过滤所需属性的类型
type FilterRequiredAttribute<Attr extends AttributeClass> = FilterOptionalAttribute<Attr> extends never ? Attr : never
  1. 要从类型转换为原始类型的类型
type ExtractPrimitiveType<A> =
  A extends StringConstructor ? string :
    A extends NumberConstructor ? number :
      A extends BooleanConstructor ? boolean :
        never
  1. 要从类转换为键值对象的类型(必需+可选)
type AttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]: ExtractPrimitiveType<Attr["valueType"]> }

type OptionalAttributeDataType<Attr extends AttributeClass> = { [K in Attr["attrName"]]?: ExtractPrimitiveType<Attr["valueType"]> }
  1. 将其粘在一起+可以推断出数组类型的东西
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的链接

2 个答案:

答案 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,
};

Playground

答案 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}}属性的每个?键,属性值的类型为KattrName仅找到T作为其ReturnType<Extract<T, { attrName: K }>["valueType"]>的{​​{1}}的一个成员。然后我们查找其Extract<T, {attrName: K}>属性,我们知道它是TKattrName中的一个(或多个)。

事实证明,这些类型中的每一个都是可调用的函数,该函数返回原始数据类型:

"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

应该帮助您为示例代码中的类构建解决方案。


好的,希望能有所帮助;祝你好运!

ReturnType