Transforming a TypeScript data type into a comparable type

I'm facing a challenge with two interfaces that represent the data returned by an API and the actual data itself.

interface APIModelCommon {
    id: number
    createdAt: string
    updatedAt: string
}

interface ModelCommon {
    id: number
    createdAt: Date
    updatedAt: Date
}

I've been struggling to write a function that properly converts the API model to the correct type. Previously, I had to resort to type assertions with eslint-disable comments, but I want to handle this issue more effectively.

The goal is to convert

interface APIArchive extends APIModelCommon
to
interface Archive extends ModelCommon
, for instance.

Every attempt I've made so far has resulted in errors, and I haven't been able to find a solution.

function correctScrapedModel<A extends APIModelCommon, T extends ModelCommon>(apiModel: A): T {
    const clone: T = (({ createdAt: _, updatedAt: __, ...o }) => ({
        ...o,
        createdAt: new Date(),
        updatedAt: new Date(),
    }))(apiModel)
    clone.createdAt = new Date(apiModel.createdAt)
    clone.updatedAt = new Date(apiModel.updatedAt)
    return clone
}

Currently, the function is generating this error:

TS2322: Type 'Omit<A, "createdAt" | "updatedAt"> & { createdAt: Date; updatedAt: Date; }' is not assignable to type 'T'. 'Omit<A, "createdAt" | "updatedAt"> & { createdAt: Date; updatedAt: Date; }' can be assigned to the constraint of type 'T', but 'T' might use a different subtype of constraint 'ModelCommon'.

The temporary fix involves using:

function correctScrapedModel<A extends APIModelCommon, T extends ModelCommon>(apiModel: A): T {
    const clone = (({ createdAt: _, updatedAt: __, ...o }) => ({
        ...o,
        createdAt: new Date(),
        updatedAt: new Date(),
    } as unknown as T))(apiModel)
    clone.createdAt = new Date(apiModel.createdAt)
    clone.updatedAt = new Date(apiModel.updatedAt)
    return clone
}

However, I prefer avoiding as unknown as TYPE statements if possible.

Is there a better approach to resolve this? If there's a way to inform Typescript that

Omit<APIModelCommon, 'createdAt' | 'updatedAt'> & { createdAt: Date, updatedAt: Date}
is equal to ModelCommon, that would likely solve the issue, but I am unaware of how to do that.

Answer №1

If you want to avoid repetition in your code, consider using generics and conditional types.

For different scenarios based on whether the generic is "Json" or "Native", specify JSON-compatible types (e.g. string) or native types (e.g. Date) accordingly. You can simplify the process by implementing a default generic parameter for one of the variations to reduce verbosity. Take a look at this example:

Check out the code in action on TS Playground

type SerializationFormat = "Native" | "Json";

type ModelCommon<Format extends SerializationFormat = "Native"> = {
  id: number;
  createdAt: Format extends "Native" ? Date : string;
  updatedAt: Format extends "Native" ? Date : string;
};

function deserializeModel(input: ModelCommon<"Json">): ModelCommon {
  return {
    id: input.id,
    createdAt: new Date(input.createdAt),
    updatedAt: new Date(input.updatedAt),
  };
}

function serializeModel(input: ModelCommon): ModelCommon<"Json"> {
  return {
    id: input.id,
    createdAt: input.createdAt.toISOString(),
    updatedAt: input.updatedAt.toISOString(),
  };
}

Answer №2

The root of the issue you're facing lies in the lack of a common type between APIArchive and Archive, or any other generic types that extend APIModelCommon and ModelCommon.

interface APIArchive extends APIModelCommon {
    foo: string;
}

interface Archive extends ModelCommon {
    foo: string;
}

This problem is exacerbated by the constraint specified on the method:

<A extends APIModelCommon, T extends ModelCommon>

When omitting "createdAt" and "updatedAt," TypeScript cannot establish the equivalence between type A and type T.

A suggested solution is to revise your models so they can have shared attributes. In the meantime, one workaround is to rely on type inference for the return type.

interface APIModelCommon {
  id: number;
  createdAt: string;
  updatedAt: string;
}

interface ModelCommon {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

interface APIArchive extends APIModelCommon {
  foo: string;
}

interface Archive extends ModelCommon {
  foo: string;
}

function correctScrapedModel<A extends APIModelCommon, T extends ModelCommon>({
  createdAt,
  updatedAt,
  ...o
}: A &
  Omit<T, "createdAt" | "updatedAt">) {
  return {
    ...o,
    createdAt: new Date(createdAt),
    updatedAt: new Date(updatedAt),
  };
}

const a : APIArchive = {} as any; // trick compiler, actual value not relevant here
const b : Archive = {} as any; // trick compiler, actual value not relevant here

const c : Archive = correctScrapedModel(a);

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

Using Angular's [ngIf], [ngIfElse], and [ngIfElseIf] functionalities enables dynamic content rendering

Currently, I have the following code snippet: <ng-container *ngIf="someCondition"> <ng-template [ngIf]="cd.typedType === 'first'" [ngIfElse]="Second"> <div class="row"> fir ...

Type narrowing is ineffective for discriminated unions in most cases

Consider the type definition provided below: type A = { a: string } | { a?: undefined; b: string } This essentially means that if you include a, it should be the only property provided. If you do not include or have a as undefined, then you also need to p ...

Make an http.patch request to the server using a Nativescript application

I am attempting to make an http.patch request to the server in my Nativescript application, which is built with Typescript and Angular2. The backend system is developed using Python(Django). Here is the code for my request: updateOrder(id, message) { ...

Activation of Angular SwUpdate deprecation

Within my Angular project, I am currently utilizing the following code snippet: this.swUpdate.available.subscribe(() => { ... }); While this code functions correctly, it does generate a warning regarding the deprecation of activated. How can I addre ...

Error: Trying to modify a property that is set as read-only while attempting to override the toString() function

I have a specific object that includes an instance variable holding a collection of other objects. Right now, my goal is to enhance this list of elements by adding a customized toString() method (which each Element already possesses). I experimented with t ...

Keep track of the input values by storing them in an array after they are

Creating an Angular application to read barcodes. barcode-scanner.component.html <form #f="ngForm" class="mt-3 text-center" id="myform" (ngSubmit)="onSubmit(f)"> <div class="text-center"> <input type="text" maxlength= ...

WebStorm is not auto-completing the Emotion Styled Components

While using @emotion/styled in WebStorm, I have noticed that there is no Intellisense for autocomplete within my style object. However, Typescript does seem to be checking to some extent: const StepTimer = styled.button({ borderRadius: 50, height: &ap ...

Encountering a TypeScript error within the queryFn while implementing Supabase authentication alongside React Toolkit Query

I've been attempting to integrate Supabase authentication with React Toolkit Query but encountering an issue with the utilization of the queryFn. Here is the code snippet that employs supabase.auth.signUp to register a user using email/password. You ...

Angular // binding innerHTML data

I'm having trouble setting up a dynamic table where one of the cells needs to contain a progress bar. I attempted using innerHTML for this, but it's not working as expected. Any suggestions on how to approach this? Here is a snippet from my dash ...

Using curly braces in a fat arrow function can cause it to malfunction

Could someone provide insight into why this code snippet functions as intended: filteredArray = contacts.filter( (contact: Contact) => contact.name.toLowerCase().includes(term.toLowerCase()) ); while this variation does not: filteredArray = contact ...

Using Vue 2 with a personalized Axios setup, integrating Vuex, and incorporating Typescript for a robust

I'm still getting the hang of Typescript, but I'm facing some challenges with it when using Vuex/Axios. Current setup includes: Vue CLI app, Vue 2, Vuex 3, Axios, Typescript At a high level, I have a custom Axios instance where I configure the ...

What is the proper way to validate a property name against its corresponding value?

Here is the structure of my User class: export class User { public id: number; //Basic information public email: string; public firstName: string; public lastName: string; //Permissions public canHangSocks: boolean; p ...

What is the relationship between an odd number and the value 1 in JavaScript that results in a 'true' outcome?

I encountered a straightforward problem, but the solution has left me perplexed. function transformArray(numbers) { // If 'i' is an odd number, i & 1 will evaluate to 1 or true return numbers.map(i => (i & 1) ? i * 3 : i * 2); } co ...

Tips for testing SSM's GetParameter with aws-sdk-client-mock in typescript

I'm looking to create a test case for the SSM GetParameterCommand request in TypeScript. Here is my code snippet: const region = "us-east-1" const awsSSMClient = new SSMClient({ region }) export async function fetchParam(parameterName: string, decry ...

Issue with rest operator behavior in TypeScript when targeting es2018

This specific code snippet functions properly in the TypeScript Playground... class Foo { constructor(...args: any[]) { } static make(...args: any[]): Foo { return new Foo(...args); } } Example However, when trying to incorpora ...

Create a randomized item for experimentation in NodeJs using an interface

Looking for a NodeJs package that can generate fake data in all required fields of a complex object described by a set of typescript interfaces, including arrays and sub-interfaces. Any recommendations? ...

Connecting Ag Grid with modules

Unable to link with modules as it's not a recognized attribute of ag-grid-angular https://i.sstatic.net/2zwY2.png <ag-grid-angular #agGrid style="width: 100%; height: 100%;" id="myGrid" class="ag-theme-balham" [mod ...

Optimizing Node.js and Express routes by separating them into individual files: a

When using Express in a Node project along with Typescript, what are the best practices for express.Router? For instance, it is recommended to follow a directory structure like this: |directory_name ---server.js |--node_modules |--routes ---in ...

Omit select dormant modules when building (Angular5)

Currently, I am collaborating on a project that is being implemented across various customer instances. Within the project, we have several lazy loaded modules, with most of them being utilized by all customers. However, there are certain modules that are ...

Beginner: Add "shared" module elements to app.module and include them in app.component.html as part of the app's layout

I am trying to incorporate three components from a "shared" module into app.component.html. Here is my current setup: <header></header> <div class="main-wrapper"> <div class="bg-trick"></div> &l ...