Typescript Generics type does not pass validation

// BaseRepository.ts
export type FieldMappings<T> = { readonly [k in keyof Required<T>]: string };

export abstract class BaseRepository<T> {
    protected abstract readonly FIELDS: FieldMappings<T>;
}

// ProductsRepository.ts
import { FieldMappings, BaseRepository } from './BaseRepository';

interface Product {
    name: string;
    price: number;
    category?: string;
}

export class ProductsRepository extends BaseRepository<Product> {
    protected readonly FIELDS = {
        name: 'product_name',
        price: 'product_price',
        category: 'product_category',
        description: 'description', // extra field
    };
}

There is no compile error when adding an additional description field to the FIELDS declaration in the ProductsRepository, even though it's not part of the Product interface.

Addition of a type declaration to the FIELDS property in the ProductsRepository, like

FIELDS: FieldMappings<Product>
, will result in a compilation error.

Avoiding redeclaration of the type for every class inheriting from BaseRepository is desired. Is this behavior possibly linked to the use of abstract? Or perhaps it relates to some settings in the tsconfig.json file? Any insights on how to rectify this issue would be appreciated.

Answer №1

Expanding on an interface or a class using extends allows you to refine the subinterface/subclass, either by introducing new properties or refining existing ones. In TypeScript, it is perfectly valid for the COLUMNS property of UserRepository to be more specialized than the COLUMNS property of RepositoryBase<User>. This is the main purpose of the extends keyword.

If you decide to define the type of the COLUMNS property in UserRepository as Record<keyof User, string> (similar to your FieldsMapping<User>) and use a new object literal during assignment, TypeScript's excess property checking will come into play, signaling any additional properties.

If this approach suits your needs, go ahead with it, even though it may seem somewhat redundant since you have to specify User both as a generic parameter in RepositoryBase<User> and in the annotation for the COLUMNS property.

If the above solution does not meet your requirements, there isn't a straightforward fix due to the complexity associated with manipulating protected properties programmatically. For instance, keyof UserRepository does not include COLUMNS, making UserRepository["COLUMNS"] invalid.

A potential workaround could involve creating a generic method like strictColumns() that only accepts a field mapping without any additional properties. The method would simply return its input, allowing you to initialize the COLUMNS property:

abstract class RepositoryBase<T> {
  protected abstract readonly COLUMNS: Record<keyof T, string>;
  strictColumns<
    U extends { [K in keyof T | keyof U]: K extends keyof T ? string : never }
  >(u: U) {
    return u;
  }
}

Implementing this method as shown below:

class UsersRepository extends RepositoryBase<User> {
  protected readonly COLUMNS = this.strictColumns({
    firstName: "first_name",
    lastName: "last_name",
    email: "email"
  });
} // works fine

class BadUsersRepository extends RepositoryBase<User> {
  protected readonly COLUMNS = this.strictColumns({
    firstName: "first_name",
    lastName: "last_name",
    email: "email",
    extra: "extra" // error! cannot assign string to never
  });
}

Although this method works, the added complexity might not justify the marginal reduction in redundancy. Nevertheless, give it a try and see if it fits your needs.

Hopefully, these suggestions are helpful. Best of luck!

Code Reference

Answer №2

const userDetails = {
    firstName: '',
    lastName: '',
    email: '',
    username: ''
};

const userInformation: User = userDetails;

In Typescript, you can make this type of 'flexible' assignment, although it enforces stricter rules during declaration.

An interesting proposal addressing this issue can be found here: https://github.com/microsoft/TypeScript/issues/12936

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

The `setState` function is failing to change the current value

I'm having an issue with setting State in the dropdown component of semantic-ui-react while using TypeScript in my code. The selected category value is always returning an empty string "". Any suggestions on how to resolve this problem? impo ...

What is the best way to ensure that dynamic routes from the server do not fail upon refreshing?

My application's permissions are dynamically received from the server. I have implemented a solution where I modify the router tree (using Router.config) before navigating to any item's URL in my navigation bar. Here is the function that sets the ...

Turning HTML into PDF using Angular 10

I am having success generating a PDF from my HTML page when the data is bound to td directly from ts. However, I face issues when trying to bind the value to an input. You can see a working example on Stackblitz. Generating a PDF for my ...

What is the Process for Integrating TypeScript into a Hook Function in SvelteKit?

In my SvelteKit application's hooks.ts file, I currently have the following code: export async function handle({ event, resolve }) { console.log(event.locals) //<-- Works fine } I am now exploring how to implement types for the event and resolve ...

What methods can we employ to ensure that Typescript adheres to the default generic argument type in this situation?

const EventKeys = { openItem: 'openItem', changeActiveItem: 'changeActiveItem', selectionToggled: 'selectionToggled', } as const type EventKeys = keyof typeof EventKeys class Test<Evmt>{ subscribe<Curren ...

What is the reason for the undefined value of "this.memberSkills"?

Encountering an issue when calling the groupSkillsByExpertise() function in ngOninit as the console.log displays this.memberSkills as undefined. However, the content of membersSkills can be displayed in the HTML template. The aim is to filter the array of ...

Why is Zod making every single one of my schema fields optional?

I am currently incorporating Zod into my Express, TypeScript, and Mongoose API project. However, I am facing type conflicts when attempting to validate user input against the user schema: Argument of type '{ firstName?: string; lastName?: string; pa ...

Guide on generating a video thumbnail using JavaScript Application

Searching for a way to easily create a thumbnail from a video for uploading alongside the video itself to a server? I've been looking for JavaScript libraries to simplify the process without much luck. The scenario involves the user selecting a video ...

Remove all input fields within an HTML file using a TypeScript method implemented in an Angular 2 component

Within my Angular project, there are several input elements in the HTML file that are not enclosed within a form tag. I am looking to create a function in the TypeScript file that will clear all of these inputs. I attempted to utilize ViewChild, but it a ...

Why is TypeScript only supporting Promise<T> params and not Promise<T1,T2>?

I have been contemplating why the Promise<T> structure does not accept two parameters, such as Promise<T1,T2>. For instance: new Promise(function(resolve,reject){ ... err ? reject(err) : resolve(val); }); => ...

Could someone provide some insights on how the ( !! ) operator functions?

Angular 6 has arrived and while browsing through a quick tutorial on Medium, I stumbled upon this interesting piece of code. else if (!!this.day4Name && !this.day5Name && days[date] !== this.day4Name) { this.day5Name = days[date]; ...

Is it possible to define a new type in TypeScript using "runtime" keys?

Illustrate with an example: class ModuleOptions { key1?: string; key2?: string; keyA?: string; keyB?: string; } class Module { static options: ModuleOptions = { key1: 'key1', key2: 'key2', keyA: 'keyA&apos ...

Using Typescript: Undefined arrays could cause issues in the array map function

Encountering a Typescript error stating that '<array name>' is possibly undefined while attempting to map over an array in Typescript. Below is an example code snippet triggering this issue: type obj = { list?: string[] }; function dem ...

What is the best way to tally the elements of a nested object within a group of objects?

I am working with an array of objects that contain nested arrays of objects, similar to what is shown in Code snippet 1. My goal is to calculate the number of records within the nested array where the parent_user_id matches the id. Based on this criteria, ...

What is the best way to invoke a TypeScript function within a jQuery function?

Is it possible to invoke a TypeScript function within a jQuery function? If so, what is the correct approach? Here is an example of my component.ts file: getCalendar(){ calendarOptions:Object = { height: 'parent', fixedWeekCount : ...

When executing tests in jest, imports from node_modules may become undefined

My jest configuration seems to be encountering an issue with resolving node_modules during execution. They are coming back as undefined... Here is a snippet from my test file: import lodash from 'lodash' it('test', () => { expect ...

Leveraging Angular 2 to retrieve information from mongoDB

I recently finished setting up my nodejs project which includes a database and some data. The database was created using the following URL: mongodb://localhost:27017/ Check out the code snippet below: var MongoClient = require('mongodb').MongoC ...

Harnessing the power of external Javascript functions within an Angular 2 template

Within the component, I have a template containing 4 div tags. The goal is to use a JavaScript function named changeValue() to update the content of the first div from 1 to Yes!. Since I am new to TypeScript and Angular 2, I am unsure how to establish comm ...

The parameter 'NextApiRequest' cannot be assigned to the parameter 'Request'

I encountered a typescript issue that states: Argument of type 'NextApiRequest' is not assignable to parameter of type 'Request'. Type 'NextApiRequest' is not assignable to type '{ url: string; }'. Types of pro ...

Using TypeScript to Bind a Class Method as a jQuery EventHandler

Currently, I am in the process of converting some JavaScript code to TypeScript and I am facing a challenge with defining the signature of a jQuery EventHandler. In the original JavaScript code, simplified to more generic terms, there is an Observer proto ...