Implementing intelligent parameter type enforcement according to configuration settings

I can't decide on a name for this concept, so please be patient while I explain it.

There are three configuration objects:

const configA = {
    type: 'A' as const,
    getPath: (query: { foo: string }) => `/${query.foo}`
}

const configB = {
    type: 'B' as const,
    getPath: (query: {}) => `/path/b`
}

const configC = {
    type: 'C' as const,
    getPath: (query: {bar: number}) => `/c/${query.bar}`
}

These configurations are combined into a master record like this:

const CONFIGS = {
    [configA.type]: configA,
    [configB.type]: configB,
    [configC.type]: configC
}

Now, the goal is to write a function that enforces type safety based on these configurations. For example, if the following code is written:

add({
    type: 'C',
    query: { foo: true }
})

TypeScript should warn about the invalidity of the query object when the type is C. However, currently no warning is issued.

An attempt was made with the following implementation:

function add(inputs: {
    type: (typeof CONFIGS)[keyof typeof CONFIGS]['type'],
    query: Parameters<(typeof CONFIGS)[keyof typeof CONFIGS]['getPath']>[0]
}) {
 const config = CONFIGS[inputs.type];
 if (!config) {
    throw new Error('Config not found');
 }

 const url = 'mysite.blah' + config.getPath(inputs.query);

 console.log('url:', url);
}

Unfortunately, this code isn't working as intended and gives a TypeScript error related to the inputs.query:

Argument of type '{} | { foo: string; } | { bar: number; }' is not assignable to parameter of type '{ foo: string; } & { bar: number; }'.

The complete code is available in the TypeScript Playground.

What is this approach called, and is there a way to resolve this issue? Thank you.

Answer №1

The issue with the call signature of add is that it lacks type safety due to the independent nature of the type and query properties within the inputs parameter. Both are defined as union types, allowing for any of three possible values each. To resolve this, you can align the type of inputs into a single union so that type and query are related:

type Inputs = {
    [P in keyof typeof CONFIGS]: {
        type: P,
        query: Parameters<typeof CONFIGS[P]["getPath"]>[0]
    }
}[keyof typeof CONFIGS]

/* type Inputs = 
  { type: "A"; query: { foo: string; }; } | 
  { type: "B"; query: {}; } | 
  { type: "C"; query: { bar: number; }; } */

declare function add(inputs: Inputs): void;

add({ type: 'C', query: { foo: true } }); // error!
add({ type: 'C', query: { bar: 123 } }); // okay

This amendment resolves the caller-side issue. However, the implementation still contains the same problem:

function add(inputs: Inputs) {
    const config = CONFIGS[inputs.type];
    if (!config) {
        throw new Error('Config not found');
    }
    const url = 'mysite.blah' + config.getPath(inputs.query); // error!
    console.log('url:', url);
};

The lack of direct support for "correlated unions" in TypeScript leads to this complication as outlined in microsoft/TypeScript#30581. The compiler analyzes a block of code like the body of add() only once, hence cannot discern the safety when using expressions like

CONFIGS[inputs.type].getPath(inputs.query)
. To circumvent this, refactoring is recommended by expressing operations through a key-value type and utilizing generic indexes, mapped types, and simple key-value types instead of unions as detailed in microsoft/TypeScript#47109.

Follow these steps to refactor and enhance the safety of your code.

...

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 error message "Value property is not found on the FilterMetadata type in the PrimeNG table" indicates that there is an issue with accessing the 'value'

While transitioning a module from Primeng 7 to Primeng 11 in conjunction with Angular 11, everything seems to be running smoothly on ng serve with all functionalities working as expected. However, upon building the project, I encounter an unexpected error. ...

Challenges Encountered when Making Multiple API Requests

I've encountered a puzzling issue with an ngrx effect I developed to fetch data from multiple API calls. Strangely, while some calls return data successfully, others are returning null for no apparent reason. Effect: @Effect() loadMoveList$: Obse ...

Error in Typescript due to delegate function not being recognized post minification

Here is a code snippet that uses delegate: <pre> $.ajax(this.validateURL, { type: "post", url: this.validateURL, data: JSON.stringify(i), contentType: "application/json; charset=utf-8", dataType: "json", success: i => t.pro ...

Experimenting with altering the heights of two Views using GestureHandler in React Native

I am currently working on a project where I need to implement height adjustable Views using React Native. One particular aspect has been causing me some trouble. I'm trying to create two stacked Views with a draggable line in between them so that the ...

Propagating numerical values through iterative iterations

I am currently facing an issue with passing values as props to a component using the forEach method in JavaScript. In addition to passing the existing values from an array, I also want to send another value that needs to be incremented by 1 for each iterat ...

Error: The render view is unable to read the 'vote' property of a null object

Within this component, I am receiving a Promise object in the properties. I attempt to store it in state, but upon rendering the view, I encounter the error message "TypeError: Cannot read property 'vote' of null." Seeking a solution to my predic ...

A guide on transitioning from using require imports to implementing ES6 imports with the concept of currying

Currently in the process of migrating a Node/Express server to TypeScript. I have been using currying to minimize import statements, but now want to switch to ES6 import syntax. How can I translate these imports to ES6? const app = require("express")(); ...

What could be the reason for encountering TypeScript within the Vue.js source code?

While exploring the vue.js source code, I stumbled upon some unfamiliar syntax that turned out to be TypeScript after further investigation. What baffled me was finding this TypeScript syntax within a ".js" file, when my understanding is that TypeScript ...

Tips for inserting a string into an array nested within an object stored in a state array

Currently, the variable sizeVariant is a string array and I am trying to append strings to it using an onClick event. The function findIndex seems to be working fine. However, there seems to be an issue with the concatenation section. It appears that using ...

Tips to avoid multiple HTTP requests being sent simultaneously

I have a collection of objects that requires triggering asynchronous requests for each object. However, I want to limit the number of simultaneous requests running at once. Additionally, it would be beneficial to have a single point of synchronization afte ...

Having Trouble Retrieving Data from Observable in Angular 2 and Typescript

I've encountered a promise error when trying to access a variable that receives data from an observable. Here's an example: Within my component, I have defined the Stat class: export class Stats { name: string; percentage: number; constru ...

Create a custom class that functions similarly to a dictionary

Is it not feasible to achieve this? interface IMap { [key: string]: string; } class Map implements IMap { public foo = "baz"; } But instead of success, I encounter the error: TS2420:Class 'Map' does not correctly implement 'IMap& ...

Attempting to render the application results in an error message stating: "Actions must be plain objects. Custom middleware should be used for asynchronous actions."

I am experiencing an issue while testing my vite + typescript + redux application to render the App component using vitest for testing. I am utilizing redux@toolkit and encountering a problem when trying to implement async thunk in the app component: Error ...

Missing from the TypeScript compilation are Angular5's polyfills.ts and main.ts files

Here is the structure of my Angular5 project. https://i.stack.imgur.com/tmbE7.png Within both tsconfig.app.json and package.json, there is a common section: "include": [ "/src/main.ts", "/src/polyfills.ts" ] Despite trying various solu ...

Understanding how to deduce parameter types in TypeScript

How can I infer the parameter type? I am working on creating a state management library that is similar to Redux, but I am having trouble defining types for it. Here is the prototype: interface IModel<S, A> { state: S action: IActions<S, A&g ...

TypeScript type that accommodates all object interfaces

I have several functions that all take the same type as input but return different types of interfaces. I'd like to define a type that can encompass all these functions, but when I try to do so with: const f: (arg: number) => Object = func; I enc ...

Enhanced VS code typings for Nuxt injected properties

My approach to injecting properties into the Nuxt TS context is as follows: ~/plugins/services.ts import Vue from 'vue'; import { errorService } from '~/services/error'; import { Plugin } from '@nuxt/types' const services: Pl ...

What is the method for obtaining a literal type through a function parameter to use as a computed property name?

Can you help me with this code snippet? const fn = (name: string) => { return { [name]: "some txt" }; }; const res = fn("books"); // books or any other string The type of res recognized by TS is: const res: { [x: string]: string ...

Ways to add items to an array adjacent to items sharing a common property value

I have an array consisting of various objects const allRecords = [ { type: 'fruit', name: 'apple' }, { type: 'vegetable', name: 'celery' }, { type: 'meat', name: 'chi ...

Tips for ensuring that functions can pass arguments with uniform overloads

I need to create a function that passes its arguments to another function, both with the same overloads. function original (a: number): boolean; function original (a: string, b: string): boolean; function original (a: number | string, b?: string): boolean ...