Leveraging the power of literal types to choose a different type of argument

My API is quite generic and I'm looking for a typed TypeScript client solution. Currently, my code looks like this:

export type EntityTypes =
    | 'account'
    | 'organization'
    | 'group'

export function getListByValue<TEntityColumns>(
  entity: EntityTypes, 
  filterColumn: keyof TEntityColumns, 
  filterValue: any) {
      apiRequest({
         entityType: entity,
         filter: {
            column: filterColumn,
            value: filterValue,
            op: '='
         }
      });
  }

While this approach works, there are some drawbacks:

  1. I need to manually set the generic parameter each time.
  2. The generic parameter is not required (although there's a workaround).
  3. There can be accidental mismatches between the generic parameter and entity type literal parameter.

Is there a way to bind the TEntityColumns type with EntityTypes into a single structure in order to make the API type safe, so that minimal typing would result in maximum autocompletion and type safety?

Update I was seeking a generic solution on how to combine literal types with type selection and thus did not include my current definition of TEntityColumns which is:

  1. Quite complex and
  2. I cannot modify it as it is part of the internal SDK

With my current definition, I am able to write code like this:

getListByValue<AccountColumns>('account', 'name', 'John');

// I could omit the generic parameter but then I lose type safety
getListByValue('account', 'name', 'John');

// Also, I can do this and the compiler will not catch the error
getListByValue<OrganizationColumns>('account', 'VAT number', '123456')

Therefore, I would like to have something similar to this (or maybe there is another option that I am unaware of):

// omit the generic
getListByValue('account', 'name', 'John');

// or just use the generic
getListByValue<AccountColumns>('name', 'John')
export declare type EntityColumns<T extends {
    [name: string]: ColumnDef;
}> = {
    [P in keyof T]: EntityColumnValue<T[P]>;
};

export declare type EntityColumnValue<T extends ColumnDef> = 
    T['type'] extends 'boolean' ? boolean | undefined : 
            T['type'] extends 'number' ? number | undefined : 
                    T['type'] extends 'number-array' ? number[] | undefined : ...

export declare type ColumnDef = {
    type: 'boolean';
    index?: boolean;
} | {
    type: 'number';
    index?: boolean | 'unique';
} | {
    type: 'number-array';
    index: boolean;
} | ...


Answer №1

Having a function with multiple modes is a common need, often leading to the realization that what's desired is a generic function. However, using generic functions with union types can sometimes result in unexpected behavior. For instance:

type MyKeyType = 'A' | 'B';

type MyTypeMap = {
    A: number,
    B: string,
};

// Just using declare since implementation details aren't important to the demo
declare function myFn<T extends MyKeyType>(type: T, data: MyTypeMap[T]): void;

myFn('A', 1); // Allowed
myFn('B', 'str'); // Allowed

// Allowed, but perhaps shouldn't be
myFn(
    Math.random() > 0.99 ? 'A' : 'B',
    'str'
);

TypeScript Playground

This demonstrates how a generic type can be inferred as a union type. It shows the challenges when multiple arguments refer to the same generic type and may lead to mismatches.

To address this issue, tying the type of multiple function arguments together by defining them as a destructured tuple and using a discriminated union type is recommended. Here's an example:

type MyKeyType = 'A' | 'B';

type MyTypeMap = {
    A: number,
    B: string,
};

// Constructing a discrimated union automatically using an
// immediately indexed mapped type
type MyFnArgs = {
    [key in MyKeyType]: [type: key, data: MyTypeMap[key]];
}[MyKeyType]

// Just using declare since implementation details aren't important to the demo
declare function myFn(...args: MyFnArgs): void;

myFn('A', 1); // Allowed
myFn('B', 'str'); // Allowed

// No longer allowed
myFn(
    Math.random() > 0.99 ? 'A' : 'B',
    'str'
);

TypeScript Playground

Using a labelled tuple ensures good intellisense while addressing the limitations of traditional generic approaches.

If you also need to associate your argument's type with a return type, function overloads or a hypothetical `oneof` operator in TypeScript could be potential solutions. Nevertheless, each approach has its own set of trade-offs and considerations.

For more information, consider exploring Feature Request: "extends oneof" generic constraint; allows for narrowing type parameters on GitHub.


In conclusion, leveraging a similar method of creating a discriminated union can facilitate the creation of a function with both arguments and return type defined, albeit with certain limitations due to TypeScript's handling of unions.

Answer №2

For anyone facing a similar issue, here's the solution to my simplified problem, inspired by @Mark Hanna's response.

If you have any ideas on how to modify the 'data' argument to match the type of the selected column instead of any, I would appreciate your input.

type MyKeyType = 'account' | 'organization';

interface AccountColumns {
    name: string;
}

interface OrgnizationColumns {
    vatRate: number;
}

type MyTypeMap = {
    'account': AccountColumns,
    'organization': OrgnizationColumns,
};

// Using declare as implementation details are not crucial to the demonstration
declare function myFn<T extends MyKeyType>(type: T, column: keyof MyTypeMap[T], data: any): void;

myFn('account', 'name', ""); // Allowed
myFn('organization', 'vatRate', ""); // Allowed

myFn('account', 'vatRate', ""); // Not allowed, as AccountColumns does not have the property 'vatRate'

Typescript 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

What is the best way to arrange map elements in a React application?

I am trying to implement filter buttons for low to high rates and high to low rates, but I am having trouble figuring it out. How can I apply the filter to display the data accordingly? The data that needs to be filtered is stored in rate.retail_rate. ty ...

What is the method for verifying the types of parameters in a function?

I possess two distinct interfaces interface Vehicle { // data about vehicle } interface Package { // data about package } A component within its props can receive either of them (and potentially more in the future), thus, I formulated a props interface l ...

Submit information by utilizing' content-type': 'application/x-www-form-urlencoded' and 'key': 'key'

Attempting to send data to the server with a content-type of 'application/xwww-form-urlencode' is resulting in a failure due to the content type being changed to application/json. var headers= { 'content-type': 'applica ...

Retrieve indexedDb quota storage data

I attempted the code below to retrieve indexedDb quota storage information navigator.webkitTemporaryStorage.queryUsageAndQuota ( function(usedBytes, grantedBytes) { console.log('we are using ', usedBytes, ' of ', grantedBytes, & ...

What causes the discrepancy in values displayed by enums in TypeScript when assigned integers in reverse order?

Recently diving into the world of TypeScript, I've been experimenting with different types in this language. One interesting data type I played with is enums. Here's an example of code I used: enum colors {red=1,green=0,blue,white}; console.lo ...

Ways to expand the width of mat-dialog-actions component in Angular 8

Is there a way to make the cancel and save buttons in the dialog window take up the entire available space? If anyone has any suggestions on how to achieve this, please let me know! ...

What is the best way to implement a <Toast> using blueprintjs?

Struggling without typescript, I find it quite challenging to utilize the Toast feature. This component appears to have a unique appearance compared to the others. Shown below is an example code. How would you convert this to ES6 equivalent? import { But ...

Too many open files error encountered in Watchpack (watcher) - NextJS

Encountering an issue with watchpack resulting in the error messages shown above while running a next app using next dev. The error message is displayed continuously on the screen as follows: Watchpack Error (watcher): Error: EMFILE: too many open files, w ...

CPU usage spikes after launching a Cordova project in Visual Studio 2015 RTM

If you're looking for the source code of the project, you can find it at https://github.com/Yaojian/Ionic-TypeScript-Starter/. I decided to create a Visual Studio project by forking https://github.com/Justin-Credible/Ionic-TypeScript-Starter/ and fol ...

`How can I eliminate all duplicate entries from an array of objects in Angular?`

arr = new Array(); arr.push({place:"1",name:"true"}); arr.push({place:"1",name:"false"}); arr.push({place:"2",name:"false"}); arr.push({place:"2",name:"false"}); arr.push({place:"3",name:"false"}); arr.push({place:"3",name:"true"}); I'm curious about ...

Yarn Plug'n'Play was unable to locate the module during the next build

Currently, I am in the process of developing a Next.js project with yarn's Plug'n'Play feature. In this project, I have created several pages and added various packages, including mathjs: '^10.3.0' to assist me in parsing user inpu ...

Creating a web application using Aframe and NextJs with typescript without the use of tags

I'm still trying to wrap my head around Aframe. I managed to load it, but I'm having trouble using the tags I want, such as and I can't figure out how to load a model with an Entity or make it animate. Something must be off in my approach. ...

What causes the distinction between entities when accessing objects through TestingModule.get() and EntityManager in NestJS?

Issue: When using TestingModule.get() in NestJS, why do objects retrieved from getEntityManagerToken() and getRepositoryToken() refer to different entities? Explanation: The object obtained with getEntityManagerToken() represents an unmocked EntityManag ...

Utilizing Vue and Typescript for efficient dependency injection

After attempting to use vue-injector, I encountered an issue as it was not compatible with my version of Vue (2.6.10) and Typescript (3.4.5). Exploring other alternatives, there seem to be limited options available. Within the realm of pure typescript, t ...

Using Angular 2 to convert and display data as a particular object type in

I have recently developed a basic application using the Angular2 tutorial as my guide. Initially, I established a straightforward "Book" model: /** * Definition of book model */ export class Book { public data; /** * Constructor for Book ...

Using Generic Types in TypeScript for Conditional Logic

To better illustrate my goal, I will use code: Let's start with two classes: Shoe and Dress class Shoe { constructor(public size: number){} } class Dress { constructor(public style: string){} } I need a generic box that can hold either a ...

Stop modal from closing in the presence of an error

My approach involves using a generic method where, upon adding a food item, a modal window with a form opens for the user to input their details. However, since backend validation for duplicate items can only be retrieved after the API call completes. I w ...

Deactivate the chosen tab by clicking the Mat-Tab button

I was trying to implement a way to disable the selected mat-tab and its elements when a button is clicked, //HTML <mat-tab-group #tabGroup> <mat-tab *ngFor="let subject of subjects" [label]="subject.name"> {{ subject.name }} ...

There was an issue encountered when creating the class: The parameters provided do not correspond to any valid call target signature

I am facing an issue with my code. Here is the scenario: export class MyClass { public name:string; public addr:string; constructor() {} } I have imported MyClass and trying to use it like this: import { MyClass } from './MyClass' ...

Angular seems to be experiencing issues with maintaining context when executing a function reference for a base class method

Imagine we have CtrlOne that extends CtrlTwo, with a componentOne instantiated in the template of CtrlOne. Here is some code to illustrate the issue: class CtrlOne extends CtrlTwo { constructor() { super(); } } class CtrlTwo { sayMyName(name: st ...