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

Unlocking the Secrets of AnimatedInterpolation Values

I have a question about how to access the value of an AnimatedInterpolation in react-native without resorting to calling private code. To achieve this, I first create an animated value and then wrap it in an interpolation like so: animated = new Anima ...

Dealing with circular dependencies in NestJS by using ForwardRef

Hey everyone, I've been running into a circular dependency issue with NestJS. I tried using the forwardref method, but it hasn't resolved the problem for me. // AuthModule @Module({ imports: [ forwardRef(() => UserModule), JwtModule ...

What is the proper way to incorporate ts-nameof in the gulp build process and encounter the error message: 'getCustomTransformers' is a compiler option that is not recognized

Utilizing ts-nameof in my TypeScript files, similar to this example in a .ts file: import 'ts-nameof'; export class MyTsNameOfTest { public testTsNameOf() { const nameString = nameof(console.log); } } My Gulp build task setup - followi ...

Express server experiencing issues with generating Swagger documentation

I've been working on an Express API and decided to implement documentation using Swagger and JSDoc. However, the documentation is not working as expected. Here's how I've set it up: docs.ts: import swaggerJSDoc, { Options } from "swagg ...

Converting Promises to Observables

Struggling with the syntax as I delve into learning Angular, I need to transform a promise into an Observable. Let me share what I've encountered: In the function getCountries (subscribed by another utility), there is a call required to fetch a list ...

I am unable to add a new property to the request object in the Express framework

My goal is to add a new property to the request object in typescript. Here's the code snippet I'm using: import { request, Request, response, Response } from "express"; ((req: Request, res: Response) => { console.log(req.user); ...

Invoke a function within the <img> tag to specify the source path

I have been attempting to achieve something similar to the following: <img id="icon" class="cercle icon" src="getIcon({{item.status}})" alt=""> This is my function: getIcon(status){ switch (status) { case 'Ongoing': ret ...

Designing a visual showcase with interactive tab links for image selection

I have been working on developing an Angular component that simulates a tab gallery functionality, inspired by this example. Below is my HTML structure: <div class="gallery-container"> <div class="display-container"> ...

What is the best way to adjust the layout of these two elements using CSS in order to display them on

I need assistance with adjusting the layout of a dropdown list next to its label in an Angular html page. <div *ngIf="this.userRole == 'myrequests'" class="col-2" [ngClass]="{ 'd-none': view != 'list&apo ...

The variable is accessed prior to being assigned with the use of the hasOwnProperty method

Continuing my journey from JavaScript to TypeScript, I find myself faced with a code that used to function perfectly but is now causing issues. Despite searching for alternative solutions or different approaches, I am unable to resolve the problem. Snippe ...

Changing a password on Firebase using Angular 5

I am in the process of developing a settings feature for user accounts on an application I've been working on. One key functionality I want to include is the ability for users to update their password directly from the account settings page. To enable ...

Attempting to execute a synchronous delete operation in Angular 6 upon the browser closing event, specifically the beforeunload or unload event

Is there a way to update a flag in the database using a service call (Delete method) when the user closes the browser? I have tried detecting browser close actions using the onbeforeunload and onunload events, but asynchronous calls do not consistently wor ...

Disregard the JSON formatting and extract solely the values

After extracting data from an API, the format of the returned information looks like this: [{"id":21},{"id":22},{"id":24}] Next, I need to send this data to a database using a different API. However, the format for sending should be like this: [21,22,24] ...

What is the best way to specify the return type of a currying function?

Check out this currying function I've implemented: export interface NewIdeaCardSubmit { title: string, description: string, categories: CategoryValues } const applyInputs = (title: string) => (description: string) = ...

Why is Typescript converting my keyof type to a never type and what steps can I take to resolve this issue?

Apologies if this question is repetitive, as I am new to TypeScript and struggling to identify related issues due to the complexity of some questions. The issue I'm facing involves TS coercing a type to never, which is confusing me. Here's the sc ...

How can TypeScript generics be used to create multiple indexes?

Here is an interface snippet: interface A { a1: { a11: string }; a2: { a21: string }; a3: { a31: string }; } I am looking to create a generic type object with indexing based on selected fields from interface A. Here is the pseudo-code exampl ...

The manager encountered an issue while querying for "Photo" data: EntityMetadataNotFoundError - no metadata could be found

I encountered an error while attempting to utilize typeorm on express: if (!metadata) throw new EntityMetadataNotFoundError(target) ^ EntityMetadataNotFoundError: Unable to locate metadata for "Photo". Below is my data source: import " ...

Navigating through React Native with TypeScript can be made easier by using the proper method to pass parameters to the NavigationDialog function

How can I effectively pass the parameters to the NavigationDialog function for flexible usage? I attempted to pass the parameters in my code, but it seems like there might be an issue with the isVisible parameter. import React, { useState } from 'rea ...

Using Array.push within a promise chain can produce unexpected results

I have developed a method that is supposed to retrieve a list of devices connected to the network that the client has access to. export const connectedDevicesCore = (vpnId: string, vpnAuthToken: string) => Service.listVPNConnectionsCore ...

Insert an HTML element or Angular component dynamically when a specific function is called in an Angular application

Currently, I am working on a component that contains a button connected to a function in the .ts file. My goal is to have this function add an HTML element or make it visible when the button is clicked. Specifically, I would like a dynamic <div> elem ...