Develop a fresh category inspired by the properties of objects

Let's tackle the challenge of constructing a new type in Typescript based on an descriptive object named schema, which contains all the requirements expressed within it.

Here is my proposed solution:

type ConfigParameter<IdType, ValueType> = Readonly<{
    name: IdType;
    type: { kind: ValueType };
}>;

type Config<T extends ReadonlyArray<ConfigParameter<string, any>>> = {
    [K in T[number]["name"]]: Extract<T[number], { name: K }>["type"]["kind"];
} extends infer O
    ? { [P in keyof O]: O[P] }
    : never;

export declare function extractType<O>(fields: O): Config<O>;

Consider the sample schema below:

const schema = [
    { name: "firstName", type: { kind: "STRING" } },
    { name: "age", type: { kind: "NUMBER" } },
    { name: "acceptedTerms", type: { kind: "BOOLEAN", optional: true } }
] as const;

We can then extract the inferred type using the following code snippet:

export const schemaExtracted = extractType(schema);

However, the extracted result may not be entirely correct:

// const schemaExtracted: {
//     firstName: "STRING"; Wrong! Should be typed as string
//     age: "NUMBER"; Wrong! Should be typed as number
//     acceptedTerms: "BOOLEAN"; Wrong! Should be typed as optional BOOLEAN
// };

Utilizing typeof to obtain a static type leads to the same error:

type SchemaTyped = typeof schemaExtracted;
// type SchemaTyped = {
//     firstName: "STRING";
//     age: "NUMBER";
//     acceptedTerms: "BOOLEAN";
// };

In trying to create a demo object using the generated type, we encounter another TypeScript error due to the mismatch in types:

const schemaDemo: SchemaTyped = {};
// const schemaDemo: {
//     firstName: "STRING";
//     age: "NUMBER";
//     acceptedTerms: "BOOLEAN";
// }
// Type '{}' is missing the following properties from type '{ firstName: "STRING"; age: "NUMBER"; acceptedTerms: "BOOLEAN"; }': firstName, age, acceptedTerms

What would be the most effective approach to rectify this issue or implement a refined solution?

Your assistance is greatly appreciated!

Answer №1

The solution involves utilizing conditional types with the infer keyword.

Additional Thoughts:

  • The generic implementation for ConfigParameter may be unnecessary if it doesn't provide extra information.
  • This current approach does not support optional types; any discoveries in this area will prompt an update to the solution.

TS playground

type ConfigParameter = Readonly<{
    name: string;
    type: { kind: "BOOLEAN" | "NUMBER" | "STRING", optional?: boolean };
}>;

type Config<T extends ReadonlyArray<ConfigParameter>> = {
    [K in T[number]["name"]]: Extract<T[number], { name: K }>["type"]['kind'] extends infer Kind
        ? Kind extends "STRING"
            ? string
            : Kind extends "BOOLEAN"
            ? boolean
            : Kind extends "NUMBER"
            ? number
            : never
        : never;
} 

const schema = [
    { name: "firstName", type: { kind: "STRING" } },
    { name: "age", type: { kind: "NUMBER" } },
    { name: "acceptedTerms", type: { kind: "BOOLEAN", optional: true } },
] as const;

type Result = Config<typeof schema>;
//   ^?

Update: The latest development includes deriving conditional types based on both the kind and optional fields.

While this method doesn't mark optional properties as "optional," it does add the undefined type to the union, providing some level of enhancement.

TS playground

Answer №2

I have a slightly different approach in mind.

In addition, I introduce the ConfigParameter data type.

type ConfigParameter = {
    name: string,
    type: {
        kind: keyof KindMap,
        optional?: boolean
    }
}

This definition doesn't require being generic. It will assist TypeScript in comprehending the object's structure for easier indexing later on.

When converting string literals like "STRING" to string, all we need is a lookup map.

type KindMap = {
    STRING: string
    NUMBER: number
    BOOLEAN: boolean
}

The usage of conditionals works but can be quite wordy at times.

Now, onto defining the Config data type itself.

type Config<O extends readonly ConfigParameter[]> = ({
    [K in O[number] as false | undefined extends K["type"]["optional"] 
      ? K["name"] 
      : never
    ]: KindMap[K["type"]["kind"]]
} & {
    [K in O[number] as true extends K["type"]["optional"] 
      ? K["name"]
      : never
    ]?: KindMap[K["type"]["kind"]]
}) extends infer U ? { [K in keyof U]: U[K] } : never

We are combining two mapped types through an intersection: one containing all properties with optional: true and the other with optional: false or optioanl: undefined.

Note: The extends infer U ? ... part is necessary for displaying the type correctly when hovering over it. Otherwise, the editor would simply show Config<readonly [{ ... which might not be very helpful.

This results in the following outcome:

export declare function extractType<O extends readonly ConfigParameter[]>(fields: O): Config<O>;

const schemaExtracted = extractType(schema);

// const schemaExtracted: {
//     firstName: string;
//     age: number;
//     acceptedTerms?: boolean | undefined;
// }

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

How can I adjust the font size property for Material UI Icons through typing?

I put together the code snippet below, and I'm trying to define a specific type for the custom iconFontSize prop. Can someone guide me on how to achieve this? import { SvgIconComponent } from '@mui/icons-material' import { Typography, Typogr ...

Initial compilation of Angular 2 project with lazy-loaded submodules fails to resolve submodules

I'm working on an Angular 2 project (angular cli 1.3.2) that is structured with multiple modules and lazy loading. In my main module, I have the following code to load sub-modules within my router: { path: 'module2', loadChildren: & ...

Two-way data binding in Angular 2 is a powerful feature that allows for

My goal is to construct a parent component called Action, which includes two child components named Infos and Localisation. I want to connect the inputs of the children with the parent model. This is the model: export class Action{ title: string; ...

Dealing with Placeholder Problems in Angular 2 Material when Using setValue

Encountering an issue with Angular2's material components when a component is updated via setValue. Check out the plnkr link below for more details... [unique PLNKR link] Observed that both the value and the placeholder are occupying the same space. ...

What is the importance of always catching errors in a Promise?

In my project, I have implemented the @typescript-eslint/no-floating-promises rule. This rule highlights code like this - functionReturningPromise() .then(retVal => doSomething(retVal)); The rule suggests adding a catch block for the Promise. While ...

Encountering ExpressionChangedAfterItHasBeenCheckedError during ngOnInit when utilizing Promise

I have a simple goal that I am working on: I want to display data obtained from a service in my component. This is how it used to work: In my component: ... dataSet: String[]; ngOnInit(){ this._service.getDataId().then(data => this.dataSet = da ...

What is the best way to retrieve distinct objects based on LocId across all locations?

Encountering an issue while working on Angular 7: unable to return distinct or unique objects based on LocId. The goal is to retrieve unique objects from an array of objects containing all Locations. allLocations:any[]=[]; ngOnInit() { this.locationsServ ...

The problem with MUI SwipeableDrawer not being recognized as a JSX.Element

Currently, I am implementing the SwipeableDrawer component in my project. However, an issue arises during the build process specifically related to the typings. I have made the effort to update both @types/react and @types/react-dom to version 18, but unf ...

Apologies, but there was an error attempting to differentiate 'nombreyo'. Please note that only arrays and iterables are permitted for this action

Encountering an error while attempting to display a class in the HTML. <li> <ul> <li *ngFor="let refac of refactormodel" > -- word_to_rename: {{refac.word_to_rename}} -- renowned_word: {{refac.renowned_word}} ...

Signing in to a Discord.js account from a React application with Typescript

import React from 'react'; import User from './components/User'; import Discord, { Message } from 'discord.js' import background from './images/background.png'; import './App.css'; const App = () => { ...

Backend communication functions seamlessly within the service scope, yet encounters obstacles beyond the service boundaries

I'm facing an issue with accessing data from my backend. Although the service successfully retrieves and logs the data, when I try to use that service in a different module, it either shows "undefined" or "Observable". Does anyone have any suggestions ...

How can I dynamically insert a variable string into a link tag using React and TypeScript?

I am just starting out with javascript and typescript, and I need to generate a link based on certain variables. I am currently facing an issue trying to insert that link into <a href="Some Link"> Some Text </a> Both the "Some Text" and "Som ...

Configuring routes for Angular4 router is a vital step in creating a

Issue: I am currently setting up routes for my application, aiming to structure the URL as https://localhost:4200/hero=id, where the 'id' will be dynamically selected. However, this setup is not functioning as expected. If I attempt to use a URL ...

Is there a way to verify if an object adheres to a specified interface?

Let's say I have a custom interface called MyInterface Is there a built-in method in TypeScript that allows checking if an object adheres to the MyInterface ? Something similar to using instanceof but for interfaces instead of classes. ...

The concept of ExpectedConditions appears to be non-existent within the context of

Just starting out with protractor and currently using version 4.0.2 However, I encountered an error with the protractor keyword when implementing the following code: import { browser } from 'protractor/globals'; let EC = protractor.Expe ...

After being awaited recursively, the resolved promise does not perform any actions

When working with the Twitter API, I need to make recursive method calls to retrieve tweets since each request only returns a maximum of 100 tweets. The process is straightforward: Call the function and await it Make an HTTP request and await that If the ...

The Vue Router hooks are not being activated within the component when utilizing Typescript

I've been pondering this issue for quite some time. Despite my extensive search efforts, I am still unable to figure out why the route-hooks are not working in my component: 1. The component is being loaded from RouterView: <router-view class="z1 ...

Utilizing TypeScript generics to accurately extract type information from state during reduction

In the context of a state reducer presented as follows: const anObject = { fruit: 'Apple', today: new Date(), } function reducer(state, stateReducer) { return stateReducer(state); } const fruit = reducer(anObject, state => state.fruit ...

TypeORM's one-to-many relationship alters the primary entity once the relationship has been established

When working on my side project, I decided to implement a friend request system using NestJS + TypeORM for the backend. However, I encountered a peculiar issue where every time I tried to associate a Friend entity with a specific user, the target field of ...

Exploring methods for interacting with and controlling structural directives in e2e testing

Background: My goal is to permutation all potential configurations of an Angular2 screen for a specified route and capture screenshots using Protractor from the following link: http://www.protractortest.org/#/debugging. Problem: I am struggling to figure ...