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

Tips for telling the difference between typescript Index signatures and JavaScript computed property names

ngOnChanges(changes: {[paramName: string]: SimpleChange}): void { console.log('Any modifications involved', changes); } I'm scratching my head over the purpose of 'changes: {[propName: string]: SimpleChange}'. Can someone cl ...

Utilize the express library (NodeJS, TypeScript) to send back the results of my function

I am curious about how I can handle the return values of my functions in my replies. In this specific scenario, I am interested in returning any validator errors in my response if they exist. Take a look at my SqlCompanyRepository.ts: async create(compan ...

What is the best way to pass a string value instead of an event in Multiselect Material UI?

Greetings, currently utilizing Material UI multi select within a React TypeScript setup. In order to modify the multi select value in the child component, I am passing an event from the parent component. Here is the code for the parent component - import ...

In Javascript, determine the root cause of a boolean expression failing by logging the culprit

Within my JavaScript code, I have a complex predicate that comprises of multiple tests chained together against a value. I am looking for a way to log the specific location in the expression where it fails, if it does fail. Similar to how testing librarie ...

The Mongoose getter function is triggering error TS2590 by generating a union type that is too intricate to be displayed

I've come across the TS2590: Expression produces a union type that is too complex to represent error while trying to compile TypeScript. The issue seems to be connected to the id's getter function idFromString, as removing the id getter prevents ...

Encountering Build Issue: "NgSemanticModule is not recognized as an NgModule" persists despite inclusion of dependencies and importing into primary module

I have posted my module, component, and package file here. I am attempting to implement a click event with ngif, but I keep encountering an error. The specific error message is "ERROR in NgSemanticModule is not an NgModule". I'm unsure if this error ...

What could be causing the React text input to constantly lose focus with every keystroke?

In my React project using Material-UI library, I have a component called GuestSignup with various input fields. const GuestSignup = (props: GuestSignupProps) => { // Component code goes here } The component receives input props defined by an ...

Unable to bind to ngModel as it returned as "undefined" in Angular 8

Whenever I bind a property to ngModel, it consistently returns undefined <div> <input type="radio" name="input-alumni" id="input-alumni-2" value="true" [(ngModel) ...

Guide to utilizing services in Angular 2

As I've developed a service with numerous variables and functions, my goal is to inject this service into multiple components. Each component should have the ability to update certain variables within the service so that all variables are updated once ...

How can we dynamically render a component in React using an object?

Hey everyone, I'm facing an issue. I would like to render a list that includes a title and an icon, and I want to do it dynamically using the map method. Here is the object from the backend API (there are more than 2 :D) // icons are Material UI Ic ...

Tips for generating a fixed-length array from multiple arrays with different lengths, focusing on selecting items from each array according to their significance

In order to create a quiz, I am looking to extract 'questions' from various 'topic' arrays. These topics are selected based on the user's preference and are used to populate a question bank for a 20-question quiz. The topics rated ...

Download a collection of base64 images as a ZIP file in Angular 4

I am currently developing an Angular2 v4 app using Typescript and I'm looking for a solution to download multiple images (in base64 format) as a Zip file. For instance, I have a sample array like this (containing fake base64 images just for illustrat ...

Guide to invoking an API in Next.js 13 by utilizing specific variables within a client component

I currently have a collection of products that are accessible on my website through a straightforward function within a server component. async function getData() { const res = await fetch(`${apiPath}`); const data = (await res.json()) as PackProps ...

Angular8: Adjusting Activity Status After Leaving Page

When performing activities like upload, download, delete, and edit, I display statuses such as 'upload started' or 'upload completed'. This works perfectly when staying on the same page. However, there are instances where a user may nav ...

Issue encountered while deploying Next.js application on vercel using the replaceAll function

Encountering an error during deployment of a next.js app to Vercel, although local builds are functioning normally. The issue seems to be related to the [replaceAll][1] function The error message received is as follows: Error occurred prerendering page &q ...

How can I retrieve the `checked` state of an input in Vue 3 using Typescript?

In my current project, I am using the latest version of Vue (Vue 3) and TypeScript 4.4. I am facing an issue where I need to retrieve the value of a checkbox without resorting to (event.target as any).checked. Are there any alternative methods in Vue tha ...

Encountering Thumbnail Type Error While Implementing Swiper in Next.js

When trying to integrate Swiper with Next.js, I ran into an issue concerning thumbnails. "onSwiper={setThumbsSwiper}" This line is causing a TypeScript error: swiper-react.d.ts(23, 3): The expected type comes from property 'onSwiper' w ...

Enhancing data binding in Angular 2.0 with string interpolation

In my script, I am working with a string that goes like this: {name} is my name. Greeting {sender} Is there a module available in Angular 2.0 that allows me to use something similar to the string.format() function in C#? I understand that it can be achie ...

Body not being checked for overloads

Is there a way for TypeScript to validate the function body against function overloads? Despite having multiple signatures, it seems that the function implementation is not being checked properly: function a(input: string): string function a(input: number ...

When using EmotionJS with TypeScript, the theme type is not properly passed to props when using styled components

CustomEmotions.d.ts import '@emotion/react'; declare module '@emotion/react' { export interface Theme { colors: { primaryColor: string; accentColor: string; }; } } MainApp.tsx import { ...