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

Generate an observable by utilizing a component method which is triggered as an event handler

My current component setup looks like this: @Component({ template: ` ... <child-component (childEvent)="onChildEvent()"></child-component> ` }) export class ParentComponent { onChildEvent() { ... } } I am aiming to ...

How to retrieve the HTTPClient value in Angular?

APIservice.ts public fetchData(owner: any) { return this.http.get(`${this.url}/${owner}`, this.httpOptions).pipe( catchError(e => { throw new Error(e); }) ); } public fetchDataById(id: number, byId:string, owner: any) { ...

The attribute 'name' cannot be found within the class 'MyComponent'

I'm a beginner in Angular2 and I have no previous knowledge of version 1. Can you help me understand why this error is occurring and guide me on how to fix it? import { Component } from 'angular2/core'; @Component ({ selector: 'my- ...

Error: The reference 'GetServerSideProps' is being incorrectly used as a type instead of a value. Perhaps you intended to use 'typeof GetServerSideProps' instead?

Index.tsx import Image from 'next/image' import Head from "next/head" import { sanityClient, urlFor } from "../sanity" import Link from 'next/link' import {Collection, address} from '../typings'; import ...

The function type '(state: State, action: AuthActionsUnion) => State' cannot be assigned to the argument

I have encountered a persistent error in my main.module.ts. The code snippet triggering the error is as follows: @NgModule({ declarations: [ PressComponent, LegalComponent, InviteComponent ], providers: [ AuthService ], imports: ...

Eliminate nested object properties using an attribute in JavaScript

I am working with a nested object structured like this const data = [ { id: '1', description: 'desc 1', data : [ { id: '5', description: 'desc', number :1 }, { id: '4', description: 'descip& ...

Using TypeScript in React, how can I implement automation to increment a number column in a datatable?

My goal is to achieve a simple task: displaying the row numbers on a column of a Primereact DataTable component. The issue is that the only apparent way to do this involves adding a data field with indexes, which can get disorganized when sorting is appli ...

Find with user-friendly input/label removal function (Ionic 2)

I have embarked on creating a recipe application where users can search for recipes by ingredients. I want to enhance the functionality of the search feature so that when users press the spacebar to enter the next input, it appears as a label below with an ...

Ensure that only numbers with a maximum of two decimal places are accepted in Angular 2 for input

On my webpage, there are several input boxes where users can enter numbers. I need to prevent them from entering more than two decimal places. Although I tried using the html 5 input Step="0.00", it didn't work as expected. I am open to any TypeScri ...

Exploring an array of objects to find a specific string similar to the one being

I recently developed a TypeScript code snippet that searches for objects in a list by their name and surname, not strictly equal: list = list.filter( x => (x.surname + ' ' + x.name) .trim() .toLowerCase() .sear ...

Applying CSS styles to a shadow DOM element will not produce the desired visual

I'm encountering an issue while attempting to apply CSS to an element within a shadow-root by adding a class to it. In my Angular component, I have the following code: CSS .test { border: 1px solid red; } TS document.getElementById('my-div&a ...

Implementing a string replacement within an array of objects using TypeScript

I have a collection of array objects displayed below [ { "subjectID": 1 "Chosen" : "{subjectsChosen:Python,java,Angular}" "password": "{studentpw:123456abcd}" }, { "subjectID": 2 ...

Typescript threw an error stating "Cannot access properties of an undefined object" in the React-Redux-axios

As a backend developer, I am not very familiar with frontend development. However, for my solo project, I am attempting to create some frontend functionalities including user login right after setting the password. Below is the code snippet from UserSlice. ...

Error encountered when trying to use the .find function in Typescript: "The expression is not callable."

Environment Details: typescript 4.5.5 Error Summary An issue occurred stating: "This expression is not callable. Each member of the union type '{ <S extends User>(predicate: (this: void, value: User, index: number, obj: User[]) => value ...

How to specify a single kind of JavaScript object using Typescript

Let's say we have an object structured as follows: const obj = [ { createdAt: "2022-10-25T08:06:29.392Z", updatedAt: "2022-10-25T08:06:29.392Z"}, { createdAt: "2022-10-25T08:06:29.392Z", animal: "cat"} ] We ...

Exploring Parquet Files with Node.js

Looking for a solution to read parquet files using NodeJS. Anyone have any suggestions? I attempted to use node-parquet but found it difficult to install and it struggled with reading numerical data types. I also explored parquetjs, however, it can only ...

Undefined value is returned for Vue 3 object property

Is there a way to extract additional attributes from the Keycloak object ? Currently, If I try, console.log(keycloak) it will display the entire keycloak object. Even after reloading, it remains in the console. However, when I do, console.log(keycloak.t ...

When you extend the BaseRequestOptions, the injected dependency becomes unspecified

Implementing a custom feature, I have chosen to extend BaseRequestOptions in Angular2 to incorporate headers for every request. Additionally, I have introduced a Config class that offers key/value pairs specific to the domain, which must be injected into m ...

Exploring the Differences between Angular's Http Module and the Fetch API

While I grasp the process Angular uses for HTTP requests, I find myself leaning towards utilizing the Fetch API instead. It eliminates the need to subscribe and unsubscribe just for a single request, making it more straightforward. When I integrated it int ...

angular directive to receive an object

When I have a table and click on a row, the information is supposed to be displayed in a different component using the @input decorator. However, instead of displaying the correct result in my other component, I am getting [object Object]. table.html < ...