Translate array into object with correct data types (type-specific method)

Welcome

In our project, we have implemented attributes support where each attribute acts as a class. These attributes include information on type, optionality, and name. Instead of creating an interface for every entity, my goal is to automate this process. With approximately 500 attributes and 100+ entities, each entity serves as a collector for various attributes.

Illustration

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]
}

Here, you can observe that I utilize valueType to determine the actual type. Additionally, I am aware of the name and whether it is optional or required based on the required attribute.

Idea and Current Approach

My approach involves iterating over the attributes array, mapping values to names, and specifying optionality.

  1. Type to filter optional attributes
export type ValueOf<T> = T[keyof T];
type FilterOptionalAttribute<Attr extends AttributeClass> = ValueOf<Attr["required"]> extends false | undefined | null ? Attr : never
  1. Type to filter required attributes
type FilterRequiredAttribute<Attr extends AttributeClass> = FilterOptionalAttribute<Attr> extends never ? Attr : never
  1. Type to convert types to primitive types
type ExtractPrimitiveType<A> =
  A extends StringConstructor ? string :
    A extends NumberConstructor ? number :
      A extends BooleanConstructor ? boolean :
        never
  1. Type to convert classes to key-value objects (with required + optional attributes)
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. Combining the above and inferring array types
type UnboxAttributes<AttrList> = AttrList extends Array<infer U> ? U : AttrList;

type DataType<AttributeList extends AttributeClass[]> = OptionalAttributeDataType<FilterOptionalAttribute<UnboxAttributes<AttributeList>>> & AttributeDataType<FilterRequiredAttribute<UnboxAttributes<AttributeList>>>

Expected Outcome

class SomeClass {
  static attributes = [AttrTest, Attr2Test]
}

// Expected output with double equals
const mapped: DataType<typeof SomeClass.attributes> == {
  test: string
  test2?: number
}

Current Behavior

Upon checking through IntelliJ IDEA Ultimate:

// The IDE displays mixed types even when inference is used
const mapped: DataType<typeof SomeClass.attributes> == {
  test: string | number
  test2: number | number
}

I have spent 5 hours attempting to resolve these issues. It seems like there's a crucial element missing from my solution. I appreciate any guidance or tips provided by those who understand where I may be going wrong.

There are two main concerns:

  • All attributes are shown as required (while 'test2' should actually be optional)
  • The types are jumbled up despite my efforts to infer them correctly

For further exploration, please refer to the TypeScript Playground

Answer №1

In this response, I will provide a simplified answer to the question by overlooking specific class definitions and variations between constructors with static properties and instances. The technique outlined below can be applied in its entirety to handle proper transformations.

Let's consider the given interface:

interface AttributeInterface {
  attrName: string;
  required?: boolean;
  valueType: StringConstructor | NumberConstructor | BooleanConstructor;
}

Now, I'll introduce a

DataType<T extends AttributeInterface>
that converts T, a collection of AttributeInterfaces, into the corresponding entity it represents. For instance, if you have an array type Arr like [Att1, Att2], you can transform it into a union by utilizing its numeric index signature: Arr[number] equals Att1 | Att2.

Here is the implementation:

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;

Before delving into the explanation, let's apply it to the following two interfaces:

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;
} */

The result looks satisfactory.

Overall, this solution involves splitting the attributes within T into two categories: required attributes (

Extract<T, { required: true }>
) and non-required attributes (
Exclude<T, { required: true }>
). By combining these two sets using intersection types, we ensure that the output meets our requirements.

Feel free to explore this approach further or adapt it for use in your sample code involving classes. Best of luck!

Link to Playground with Original Code

Answer №2

I managed to find a different solution...

// Define constructor type
type Constructor<T> = new (...args: any[]) => T;

// Define supported attribute types
type SupportTypes = [String, Number, Boolean];

// Attribute class definition
class AttributeClass<K extends string, T extends SupportTypes[number], R extends boolean = false> {
  constructor(
    readonly attrName: K,
    readonly valueType: Constructor<T>,
    readonly required?: R,
  ) {
  }
}


// Define test attributes
const AttrTest = new AttributeClass('test', String, true);
const Attr2Test = new AttributeClass('test2', Number);

const attributes = [AttrTest, Attr2Test];

// Unwrap instance of AttributeClass into an 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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Removing spaces within brackets on dynamic properties for objects can be achieved by utilizing various methods such as

I've encountered an issue with my code that involves getting spaces within square brackets for the dynamic properties of an object. Even after searching through Code Style/Typescript/Spaces, I couldn't find any settings to adjust this. Could thes ...

Submitting the object in the correct format for the Firebase database

My goal is to structure the Firebase database in the following way: "thumbnails": { "72": "http://url.to.72px.thumbnail", "144": "http://url.to.144px.thumbnail" } However, I am struggling to correctly set the keys '72' and '144&apos ...

Incorporating Sass into a TypeScript-enabled Create React App

Having recently transferred a create-react-app to typescript, I've encountered an issue where my scss files are not being recognized in the .tsx components. The way I'm importing them is as follows: import './styles/scss/style.scss'; ...

Accessing an external API through a tRPC endpoint (tRPC Promise is resolved before processing is complete)

I want to utilize OpenAI's API within my Next.js + tRPC application. It appears that my front-end is proceeding without waiting for the back-end to finish the API request, resulting in an error due to the response still being undefined. Below is my e ...

Modifying the <TypescriptModuleKind> setting for typescript transpilation in project.csproj is not supported in Visual Studio 2017

I recently encountered an issue with changing the module kind used by the transpiler in Visual Studio. Despite updating the <TypescriptModuleKind> in the project's project.csproj file from commonjs to AMD, the transpiler still defaults to using ...

Is there a way to set an antd checkbox as checked even when its value is falsy within an antd formItem?

I'm currently looking to "invert" the behavior of the antd checkbox component. I am seeking to have the checkbox unchecked when the value/initialValue of the antD formItem is false. Below is my existing code: <FormItem label="Include skills list ...

Setting the paths property in a project with multiple tsconfig.json files: a step-by-step guide

My file structure is organized as follows: |__ app1/ | |__ tsconfig.json |__ utilities/ | |__ files.ts |__ base-tsconfig.json I have defined the paths property in base-tsconfig.json like this: "compilerOptions": { "baseUrl": ".", "pa ...

What could be causing the module version discrepancy with the package.json file I created?

Recently, I created a project using create-next-app and decided to downgrade my Next.js version to 12. After that, I proceeded to install some necessary modules using Yarn and specified the versions for TypeScript, React, and others. During this setup, I b ...

Passing parameters to an Angular 2 component

When it comes to passing a string parameter to my component, I need the flexibility to adjust the parameters of services based on the passed value. Here's how I handle it: In my index.html, I simply call my component and pass the required parameter. ...

Error: Attempting to add types to an object returned from the .map function in JSX Element

When attempting to include the item type in the object returned from the .map function, I encountered a JSX error. I tried specifying item: JSX.Element as the Item type, but this didn't resolve the issue. Can someone provide clarity on this matter? Th ...

Pagination in PrimeNG datatable with checkbox selection

I am currently working on incorporating a data table layout with pagination that includes checkbox selection for the data. I have encountered an issue where I can select data on one page, but when I navigate to another page and select different data, the s ...

Angular: Navigating to a distinct page based on an API response

In order to determine which page to go to, based on the response from an API endpoint, I need to implement a logic. The current API response includes an integer (id) and a string (name). Example Response: int: id name: string After making the API call, I ...

Using TypeScript with Redux for Form Validation in FieldArray

My first time implementing a FieldArray from redux-form has been quite a learning experience. The UI functions properly, but there seems to be some performance issues that I need to investigate further. Basically, the concept is to click an ADD button to i ...

Trouble with implementing a custom attribute directive in Angular 4 and Ionic 3

Hello, I am currently working on implementing a search input field focus using a directive that has been exported from directives.module.ts. The directives.module is properly imported into the app.module.ts file. However, when attempting to use the direc ...

Is it possible to deduce Typescript argument types for a specific implementation that has multiple overloaded signatures?

My call method has two signatures and a single implementation: call<T extends CallChannel, TArgs extends CallParameters[T]>(channel: T, ...args: TArgs): ReturnType<CallListener<T>>; call<T extends SharedChannel, TArgs extends SharedPar ...

What is the best approach to creating an array within my formgroup and adding data to it?

I have a function in my ngOnInit that creates a formgroup: ngOnInit() { //When the component starts, create the form group this.variacaoForm = this.fb.group({ variacoes: this.fb.array([this.createFormGroup()]) }); createFormGroup() ...

Leveraging latitude and longitude data from an API to create circles on the AGM platform

I need assistance with utilizing location data from recent earthquake events to center a circle on Angular Google Maps. Can anyone provide guidance on how to achieve this? The API call provides the following data: 0: --geometry: ---coordinates: Array( ...

Angular 11 is throwing an error stating that the type 'Observable<Object>' is lacking certain properties as required by its type definition

My code is producing the following error: TS2739 (TS) Type 'Observable<Object>' is missing the following properties from type 'WeatherForecast': ID, date, temperatureC, temperatureF, summary I'm puzzled as to why this error ...

Using T and null for useRef in React is now supported through method overloading

The React type definition for useRef includes function overloading for both T|null and T: function useRef<T>(initialValue: T): MutableRefObject<T>; // convenience overload for refs given as a ref prop as they typically start with a null ...

Currently attempting to ensure the type safety of my bespoke event system within UnityTiny

Currently, I am developing an event system within Unity Tiny as the built-in framework's functionality is quite limited. While I have managed to get it up and running, I now aim to enhance its user-friendliness for my team members. In this endeavor, I ...