Mastering the art of constraining TypeScript function parameters using interface properties

Hey there, I've been exploring ways to restrict a function parameter so that it only accepts "strings" related to interface properties, similar to what I achieved in the validate fields function:

Please note: The TypeScript code provided here is simply for illustration purposes.

index.d.ts

export interface FieldErrors {
  errors: Array<FieldError>;
}

export type FieldsErrors<TForm> = {
  [k in keyof TForm]: FieldErrors;
}

export interface ValidateFieldsCallback<TForm> { (fieldsErrors: FieldsErrors<TForm>, values: TForm): void; };

export interface FormShape<TForm> {
  getFieldValue(fieldName: 'documentNumber' | 'userName'): void; // HOW TO FIX THIS
  validateFields(callback: ValidateFieldsCallback<TForm>): void;
  validateFields(fieldsNames: Array<string>, callback: ValidateFieldsCallback<TForm>): void;
}

example.ts

interface SignupForm {
    documentNumber: number;
    userName: string;
}

const testForm = <FormShape<SignupForm>>{};

testForm.validateFields((values) => {
    console.log(values.documentNumber); // OK
    console.log(values.userName); // OK
    console.log(values.other); // ERROR
});

// THIS SHOULD BE FIXED
const documentNumber = testForm.getFieldValue('documentNumber');

From the example above, you can see that while I have managed to define the parameter fieldsErrors in the validateFields callback, I now need to address the getFieldValue function to only accept a "valid" field name corresponding to the interface properties, and also return the appropriate type based on the interface rather than void.

I would greatly appreciate any assistance with this matter.

Answer №1

When working with mapped types, the keyof T comes into play. This type can be used in any context where a type is required and represents a union of all keys of type T, which appears to be exactly what you need.

If you want the return type of a function to match the type of a specific field, you must introduce a type parameter in the function and use a type query to fetch the type of that particular field.

export interface FormShape<TForm> {
    getFieldValue<K extends keyof TForm>(fieldName: K): TForm[K]; // Only accepts keys from TForm and returns the corresponding value of the field
    validateFields(callback: ValidateFieldsCallback<TForm>): void;
    validateFields(fieldsNames: Array<string>, callback: ValidateFieldsCallback<TForm>): void;
}

When handling arrays, there are multiple approaches available. For simple arrays, consider passing the field names as an array itself.

export interface FormShape<TForm> {
    getFieldValue<K extends keyof TForm>(...fieldName: K[]): TForm[K][] ; 
    validateFields(callback: ValidateFieldsCallback<TForm>): void;
    validateFields(fieldsNames: Array<string>, callback: ValidateFieldsCallback<TForm>): void;
}

To maintain type safety at each index, it's ideal to accept tuples as input and return tuples as well. This can be achieved through different overloads:

export interface FormShape<TForm> {
    getFieldValue<K extends keyof TForm, K1 extends keyof TForm, K2 extends keyof TForm>(fieldName: [K, K1, K2]): [TForm[K], TForm[K1], TForm[K2]]; 
    getFieldValue<K extends keyof TForm, K1 extends keyof TForm>(fieldName: [K, K1]): [TForm[K], TForm[K1]]; 
    getFieldValue<K extends keyof TForm>(fieldName: [K]): [TForm[K]]; 
    validateFields(callback: ValidateFieldsCallback<TForm>): void;
    validateFields(fieldsNames: Array<string>, callback: ValidateFieldsCallback<TForm>): void;
}

Alternatively, instead of returning an array, another approach could be to return an object containing the specified keys:

export interface FormShape<TForm> {
    getFieldValues<K extends keyof TForm>(...fieldName: K[]): { [P in K]: TForm[P] }; // Accepts only keys of TForm and returns the value of the field
    validateFields(callback: ValidateFieldsCallback<TForm>): void;
    validateFields(fieldsNames: Array<string>, callback: ValidateFieldsCallback<TForm>): void;
}

//Usage
const documentNumber = testForm.getFieldValues('documentNumber', 'userName'); //{documentNumber: number; userName: string;}

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

Issue TS2769: No matching overload found for this call. The type 'string | null' cannot be assigned to type 'string | string[]'

export class AuthService { constructor(private http: HttpClient, private webService: WebRequestService, private router: Router) { } login(email: string, password: string) { return this.webService.login(email, password).pipe( shareReplay(), ...

Common mistakes made while working with decorators in Visual Studio Code

Having trouble compiling TypeScript to JavaScript when using decorators. A persistent error message I encounter is: app.ts:11:7 - error TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the ' ...

Are there any @types available for browser extension objects that are interoperable?

I am in the process of developing a browser extension that would work seamlessly on Edge, Chrome, and Firefox by utilizing Typescript. After coming across an article discussing interoperable browser extensions, I stumbled upon a code snippet: window.brow ...

Error: Module not found '!raw-loader!@types/lodash/common/array.d.ts' or its type declarations are missing

I encountered a typescript error while building my NEXT JS application. The error message was: Type error: Cannot find module '!raw-loader!@types/lodash/common/array.d.ts' Below is the content of my tsConfig.json file: { "compilerOptions& ...

The TS2583 error in TypeScript occurs when it cannot locate the name 'Set' within the code

Just started my Typescript journey today and encountered 11 errors when running tsc app.ts. Decided to tackle them one by one, starting with the first. I tried updating tsconfig.json but it seems like the issue lies within node_modules directory. Any help ...

You won't find the property 'includes' on a type of 'string[]' even if you're using ES7 features

I encountered a similar issue on another page where it was suggested to modify the lib in tsconfig.josn. However, even after changing compile to es7, the same error kept appearing and the project couldn't be compiled or built. { "compileOnSave": ...

Angular 4: Retrieving the selected element's object from a collection of elements

In my x.component.html file, I have a list of objects that are being displayed in Bootstrap cards like this: <div *ngFor="let item of items | async"> <div class="row"> <div class="col-lg-6"> <div class="card ...

I'm unsure how to utilize the generic type in this particular scenario. It's a bit confusing to me

Recently, I delved into TypeScript generics and applied them in specific scenarios. However, I encountered some challenges. While working with two different interfaces, I faced a need for flexibility. For instance, I needed to make server requests. func ...

Issue with NgModule in Angular application build

I'm facing an issue with my Angular application where the compiler is throwing errors during the build process. Here's a snippet of the error messages I'm encountering: ERROR in src/app/list-items/list-items.component.ts:9:14 - error NG6002 ...

Typescript struggling to load the hefty json file

Currently, I am attempting to load a JSON file within my program. Here's the code snippet that I have used: seed.d.ts: declare module "*.json" { const value: any; export default value; } dataset.ts: import * as data from "./my.json" ...

How can we ensure file uploads are validated when using class-validator?

Recently, I've been utilizing the wonderful class-validator package to validate data. One specific validation task I'm working on is validating a file upload, ensuring that the file is not empty and ideally confirming that it is an image file. H ...

Could you explain the significance of the ^ symbol preceding a software version number?

Considering updating a package in my application, specifically the "@types/react-router-dom" from version "4.3.1" to "5.0.0". However, I'm hesitant as it is a large project and I don't want to risk breaking anything. While reviewing the package. ...

Basic cordova application that transfers data from one page to another using typescript

Currently, I am developing an Apache Cordova application using TypeScript. However, I am facing a challenge in passing information from one HTML page to another using TypeScript. I would appreciate it if someone could guide me on the steps needed for nav ...

Updating the text of a Mat-Label dynamically without the need to reload the page

In my application, there is a mat-label that shows the Customer End Date. The end date is fetched initially through a GET request to an API. Let's say the end date is 16-05-2099, which is displayed as it is. There is also a delete button implemented f ...

Use TypeScript to cast the retrieved object from the local storage

const [savedHistory, setSavedHistory] = useState(localStorage.getItem('history') || {}); I'm facing an issue in TypeScript where it doesn't recognize the type of 'history' once I fetch it from localStorage. How can I reassign ...

The process of linking a Json response to a table

In my products.components.ts class, I am retrieving Json data into the variable this.Getdata. ngOnInit() { this._service.getProducts(this.baseUrl) .subscribe(data => { this.Getdata=data this.products=data alert(JSON.stringify(this.Getdata)); ...

Rxjs: accessing the most recent value emitted by an observable

As shown in the demo and indicated by the title const { combineLatest, interval, of } = rxjs; const { first, last, sample, take, withLatestFrom } = rxjs.operators; const numbers = interval(1000); const takeFourNumbers = numbers.pipe(take(4)); takeFourNu ...

Combine the array elements by date in Angular, ensuring no duplicates are present

How can array data be merged based on the date while avoiding duplicates? See the code snippet below: [ { date: [ '2019-12-02 08:00:00', '2019-12-03 08:00:00' ], upload:["47.93", "47.46", "47.40", "47.29" ], download: ["43.90", ...

Stop allowing the entry of zero after a minus sign

One of the features on our platform allows users to input a number that will be automatically converted to have a negative sign. However, we want to ensure that users are unable to manually add a negative sign themselves. We need to find a solution to pre ...

The Art of Dynamic Component Manipulation in Angular

The current official documentation only demonstrates how to dynamically alter components within an <ng-template> tag. https://angular.io/guide/dynamic-component-loader My goal is to have 3 components: header, section, and footer with the following s ...