Creating a new instance of a class with a different type that adheres to the specifications

I am looking to create various objects that need to adhere to a predefined type, but each object should have its own unique structure. Additionally, I want the ability to ensure conformance to the predefined type at the point of object creation.

Let's consider the code below, which converts a structure defining APIs into the actual APIs themselves, all statically type-checked:

type ApiTemplate = {
  [funcName: string]: {
    call: (data: any) => void;
    handler: (data: any) => void;
  }
};

const apiConfig1 = /* see below */;
const apiConfig2 = /* see below */;
const apiConfig3 = /* see below */;

function toApi<T extends ApiTemplate>(
  config: T
): { [n in keyof T]: T[n]["call"] } {
  return Object.fromEntries(
    Object.entries(config).map(
      ([funcName, apiDef]) => [funcName, apiDef.call]
    )
  ) as any;
}

const api1 = toApi(apiConfig1);
const api2 = toApi(apiConfig2);
const api3 = toApi(apiConfig3);

api1.func1(2); // statically typed function signature

I aim to define each apiConfig* object in a separate file and have static type checking available while coding these objects. I also want the compiler to catch any errors within the objects. If I did not need this type checking, I could define APIs like this:

const apiConfig1 = {
  func1: {
    call: (count: number) => console.log("count", count),
    handler: (data: any) => console.log(data),
  },
  func2: {
    call: (name: string) => console.log(name),
    handler: (data: any) => console.log("string", data),
  },
};

However, if there is an error in constructing the object, such as misspelling 'handler' as 'handle', TypeScript will report the error at the call to toApi(), which is not ideal.

In my attempts to address this issue, I encountered the following challenges:

const apiConfig1 = (function <T extends ApiTemplate>(): T {
  return {
    func1: {
      call: (count: number) => console.log("count", count),
      handler: (data: any) => console.log(data),
    },
    func2: {
      call: (name: string) => console.log(name),
      handler: (data: any) => console.log("string", data),
    },
  };
})();

This resulted in the error message:

Type '{ func1: { call: (count: number) => void; handler: (data: any) => void; };
func2: { call: (name: string) => void; handler: (data: any) => void; }; }'
is not assignable to type 'T'.
  '{ func1: { call: (count: number) => void; handler: (data: any) => void; };
  func2: { call: (name: string) => void; handler: (data: any) => void; }; }'
  is assignable to the constraint of type 'T', but 'T' could be instantiated
  with a different subtype of constraint 'Template'.ts(2322)

And when trying this approach:

const apiConfig1 = (function <T>(): T extends ApiTemplate ? T : never {
  return {
    func1: {
      call: (count: number) => console.log("count", count),
      handler: (data: any) => console.log(data),
    },
    func2: {
      call: (name: string) => console.log(name),
      handler: (data: any) => console.log("string", data),
    },
  }
});

This produced the error message:

Type '{ func1: { call: (count: number) => void; handler: (data: any) => void; };
func2: { call: (name: string) => void; handler: (data: any) => void; }; }'
is not assignable to type 'T extends ApiTemplate ? T : never'.ts(2322)

Is there a solution in TypeScript to achieve what I intend?

NOTE: The method below removes static type-checking from the generated API by toApi() because it discards the specific type of each object:

const apiConfig1: ApiTemplate = /* ... */;

Answer №1

To simplify the code, you can use a basic type annotation:

type ApiTemplate = {
  [methodName: string]: {
    call: (data: any) => void;
    handler: (data: any) => void;
  }
};

const apiConfig2: ApiTemplate = {
  method1: {
    call: (value: number) => console.log("value", value),
    handle: (result: any) => console.log(result),
  },
  method2: {
    call: (input: string) => console.log(input),
    handle: (output: any) => console.log("text", output),
  },
};

If you change handler to handle, any typos will be highlighted immediately.

Answer №2

After experimenting, I discovered a technique to achieve this goal by creating a specific function that not only confirms the base type of its input but also preserves the initial type:

function validateTemplate<T extends ApiTemplate>(template: T) {
  return template;
}

Next, pass the object as an argument to the newly created function:

const apiConfiguration = validateTemplate({
  func1: {
    call: (count: number) => console.log("count", count),
    handler: (data: any) => console.log(data),
  },
  func2: {
    call: (name: string) => console.log(name),
    handler: (data: any) => console.log("string", data),
  },
  func3: { // <-- encountering errors due to missing 'call' property
    handler: (data: any) => console.log("any", data)
  }
});

Despite finding this workaround, I am still puzzled by the absence of a more straightforward solution.

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

Narrowing down types within an array of objects

I am encountering an issue with the following code snippet: const f = (x: unknown) => { if (!x || !Array.isArray(x)) { throw new Error("bad"); } for (const y of x) { if (!y || typeof y !== "object") { throw new E ...

The 'items' property cannot be linked to 'virtual-scroller' as it is not a recognized attribute

I'm currently facing an issue with integrating virtual scroll into my Ionic 4 + Angular project. Previously, I relied on Ionic's implementation of virtual scroll (ion-virtual-scroll) which worked well initially. However, I encountered a major dr ...

Is there a way to transfer ngClass logic from the template to the TypeScript file in Angular?

I am implementing dropdown filters for user selection in my Angular application. The logic for adding classes with ngClass is present in the template: <div [ngClass]="i > 2 && 'array-design'"> How can I transfer this ...

Using useRef with setInterval/clearInterval in React with TypeScript

In my code, I am implementing a useRef object to store a NodeJS.Timeout component, which is the return type of setInterval(). However, when I attempt to use clearInterval later on, I encounter an error (shown below) on both instances of intervalRef.current ...

What is the best way to handle an OR scenario in Playwright?

The Playwright documentation explains that a comma-separated list of CSS selectors will match all elements that can be selected by one of the selectors in that list. However, when I try to implement this, it doesn't seem to work as expected. For exam ...

The onInit Observable subscription will only execute a single time

Within my table, I have a list of names and an input tag that filters the content of the table when its value changes. The data is retrieved from an HTTP request to a service. I am encountering three different scenarios: 1- If I subscribe to this.ds.getD ...

Why is it that the Jasmine test is unsuccessful even though the 'expected' and 'toBe' strings appear to be identical?

I have been developing a web application using Angular (version 2.4.0) and TypeScript. The application utilizes a custom currency pipe, which leverages Angular's built-in CurrencyPipe to format currency strings for both the 'en-CA' and &apos ...

Typescript does not allow for extending an interface with a data property even if both interfaces have the same data type

I've encountered a peculiar problem with Typescript (using Visual Studio 2012 and TypeScript v0.9.5) that I could use some help clarifying. The code snippet below functions correctly: interface IA { data: any; } interface IB { data: any; } ...

Grunt Typescript is encountering difficulty locating the angular core module

Question Why is my Grunt Typescript compiler unable to locate the angular core? I suspect it's due to the paths, causing the compiler to not find the libraries in the node_modules directory. Error typescript/add.component.ts(1,25): error TS23 ...

Office-Js Authentication for Outlook Add-ins

I am currently developing a React-powered Outlook Add-in. I kickstarted my project using the YeomanGenerator. While working on setting up authentication with the help of Office-Js-Helpers, I faced some challenges. Although I successfully created the authen ...

What impact does nesting components have on performance and rendering capabilities?

Although this question may appear simple on the surface, it delves into a deeper understanding of the fundamentals of react. This scenario arose during a project discussion with some coworkers: Let's consider a straightforward situation (as illustrat ...

Ensure all fields in an interface are nullable when using TypeScript

Is it possible to create type constraints in TypeScript that ensure all fields in an interface have a type of null? For example, if I want to write a validation function that replaces all false values with null, how can I achieve this? interface y { ...

In Firebase, the async function completes the promise even as the function body is still executing

I'm currently facing an issue with an async function I've written. It takes an array of custom objects as an argument, loops through each object, retrieves data from Firestore, converts it into another custom object, and adds it to an array. The ...

"Exploring the capabilities of Rxjs ReplaySubject and its usage with the

Is it possible to utilize the pairwise() method with a ReplaySubject instead of a BehaviorSubject when working with the first emitted value? Typically, with a BehaviorSubject, I can set the initial value in the constructor allowing pairwise() to function ...

Overlooking errors in RxJs observables when using Node JS SSE and sharing a subscription

There is a service endpoint for SSE that shares a subscription if the consumer with the same key is already subscribed. If there is an active subscription, the data is polled from another client. The issue arises when the outer subscription fails to catch ...

Angular Signals: How can we effectively prompt a data fetch when the input Signals undergo a change in value?

As I delve into learning and utilizing Signals within Angular, I find it to be quite exhilarating. However, I have encountered some challenges in certain scenarios. I am struggling to come up with an effective approach when dealing with a component that ha ...

What steps should I follow to ensure that TypeScript is aware of the specific proptypes I am implementing?

Is there a way to instruct TypeScript on the prop types that a component is receiving? For example, if multiple is set to true, I would like TypeScript to expect that selectValue will be an array of strings. If it's not present, then TypeScript should ...

Passing the state variable from a hook function to a separate component

I have a hook function or file where I need to export a state named 'isAuthenticated'. However, when I try to import this state using import {isAuthenticated} from '../AuthService/AuthRoute', I encounter an import error. My goal is to m ...

In Typescript, null values are allowed even when the type is set to be non-nullable

Can someone explain why the code below allows for null in typescript, even though number is specified as the type: TS playground // Not sure why null is accepted here when I've specified number as the type const foo = (): number => 1 || null ...

deliver a precise observable

Recently, I spent hours following a tutorial on jwt refresh tokens, only to discover that the code was outdated and some changes were required. As a result, I created an interceptor which encountered an issue with the Observable component, leaving me unsur ...