Ensuring TypeScript object types are safe by requiring all keys to be within an array of values

When working with Typescript ^3.8, we have an interface defined as follows:

interface IEndpoint { method: 'get'|'put'|'post'|'patch'|'delete', path: string }

Additionally, there is a constant declared like this:

const endpoint = { method: 'get', path: '/first/:firstId/second/:secondId' }

In this scenario, :firstId and :secondId are dynamic path parameters that will be provided at runtime. A function exists that takes the endpoint and a parameter values object to build the final URL.

function buildEndpointUrl(endpoint: IEndpoint, map: {[key: string]: string}): string;

For example:

// This will yield url '/first/123/second/456'
const url = buildEndpointUrl(endpoint, {firstId: '123', secondId: '456'});

The issue arises when invalid data is passed as the second parameter. How can we define IEndpoint and buildEndpointUrl to ensure that the compiler throws an error if a required key is missing in the object?

A potential solution attempted was:

interface IEndpoint<T extends ReadonlyArray<string>> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: string
}

const endpoint: IEndpoint<['firstId', 'secondId']> = {...};

function buildEndpointUrl<T extends ReadonlyArray<string>>(
  endpoint: IEndpointConfig<T>, 
  map: {[key: T[number]]: string} // compiler error
);

However, the last line triggers a compiler error:

TS1023: An index signature parameter must be either "string" or "number"

The expectation was for T[number] to be treated as equivalent to string due to

T extends ReadonlyArray<string>
, but it seems not to be the case. What adjustments should be made in the definition to enhance type safety?

Answer №1

To replace an index signature, utilize a mapped type instead. The built-in mapped type Record is suitable for this task.

export interface IEndpoint<T extends ReadonlyArray<string>> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: string
}

const endpoint: IEndpoint<['firstId', 'secondId']> =  { method: 'get', path: '/first/:firstId/second/:secondId' };

declare function buildEndpointUrl<T extends ReadonlyArray<string>>(
  endpoint: IEndpoint<T>, 
  map: Record<T[number],string> // compiler error
): void;

const b = buildEndpointUrl(endpoint, { firstId: "", secondId:"", test: "" })

Explore Here

In version 4.1, you have the option to leverage template literal types for extracting parameters from the path string.

export interface IEndpoint<T extends string> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  path: T
}

type ExtractParameters<T extends string> = 
  T extends `${infer Prefix}/:${infer Param}/${infer Suffix}` ? Record<Param, string> & ExtractParameters<Suffix> & [Prefix, Suffix, Param] :
  T extends `${infer Prefix}/:${infer Param}` ? Record<Param, string>  :
  T extends `:${infer Param}`? Record<Param, string> :
  { T: T}

type X = "second/:secondId" extends `${infer Prefix}/:${infer Param}/${infer Suffix}` ? [Prefix, Param, Suffix] : "";
type Y = ExtractParameters<"/first/:firstId/second/:secondId">

const endpoint =  { method: 'get', path: '/first/:firstId/second/:secondId' } as const

declare function buildEndpointUrl<T extends string>(
  endpoint: IEndpoint<T>, 
  map: ExtractParameters<T>
): void;

const b = buildEndpointUrl(endpoint, { firstId: "", secondId:"", test: "" })

Check it out

Answer №2

Great progress, keep going:

type Parameters = ReadonlyArray<string>;

interface IParameters<T extends Parameters> { 
  method: 'get'|'put'|'post'|'patch'|'delete', 
  endpoint: string
}

function createEndpointUrl<T extends Parameters>(
  endpoint: IParameters<T>, 
  mapping: {[key in T[number]]: string} // Ensure proper mapping for accurate results
) {}

const endpoint: IParameters<['first', 'second']> = {
    method: "get",
    path: "",
};

createEndpointUrl(endpoint, { // unsuccessful attempt
    first: "v1",
    p2: "v2",
});

createEndpointUrl(endpoint, { // successful execution
    first: "v1",
    second: "v2",
});

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

Is it possible to deactivate the error message related to "Unable to ascertain the module for component..."?

I recently incorporated a new component into my TypeScript2+Angular2+Ionic2 project. Right now, I have chosen not to reference it anywhere in the project until it is fully developed. However, there seems to be an error thrown by Angular/ngc stating "Cannot ...

Having trouble accessing a custom factory within a directive in Angular using TypeScript

Having some trouble with my injected storageService. When trying to access it in the link function using this.storageService, I'm getting an undefined error. Any assistance on this issue would be greatly appreciated. module App.Directive { import ...

What is the best way to simulate a constructor-created class instance in jest?

Suppose there is a class called Person which creates an instance of another class named Logger. How can we ensure that the method of Logger is being called when an instance of Person is created, as shown in the example below? // Logger.ts export default cl ...

React components need to refresh after fetching data from an API

I am currently working on a React application using TypeScript and integrating JSONPlaceholder for simulating API calls. I have successfully set up everything I need, but I am encountering an issue with re-rendering components that display response data fr ...

Is there a way to restrict the return type of a function property depending on the boolean value of another property?

I'm interested in creating a structure similar to IA<T> as shown below: interface IA<T> { f: () => T | number; x: boolean } However, I want f to return a number when x is true, and a T when x is false. Is this feasible? My attempt ...

In Typescript, encountering a member of a union type with an incompatible signature while utilizing the find method on an array of

I need to verify if a specific value exists within an array of objects. The structure of my array is as follows: [ 0: { id: 'unique_obj_id', item: { id: 'unique_item_id', ... }, ... }, 1: {...} ] The objects in the ar ...

Encountering difficulties while attempting to deploy image on kubernetes due to issues with packaging structure

After successfully building with the dockerfile provided below, I encountered an issue when trying to deploy my application on EKS. FROM node:12 # Create app directory WORKDIR /usr/src/app COPY udagram-feed/package*.json ./ RUN npm ci # Bundle app sou ...

Is there a way to determine if a browser's Storage object is localStorage or sessionStorage in order to effectively handle static and dynamic secret keys within a client?

I have developed a customizable storage service where an example is getExpirableStorage(getSecureStorage(getLocalStorage() | getSessionStorage())) in typescript/javascript. When implementing getSecureStorage, I used a static cipher key to encrypt every ke ...

Easy Steps to Simplify Your Code for Variable Management

I currently have 6 tabs, each with their own object. Data is being received from the server and filtered based on the tab name. var a = {} // First Tab Object var b = {} // Second Tab Object var c = {} // Third Tab Object var d = {}// Fou ...

How to share data between two different components in Angular 6

I have a component called course-detail that fetches data (referred to as course) from my backend application. I want to pass this data to another component named course-play, which is not directly related to the course-detail component. The goal is to dis ...

Alert: Attempting to access an undefined value in an indexed type

I would like to find a way in Typescript to create a hashmap with indexable types that includes a warning when the value could potentially be undefined during a lookup. Is there a solution for this issue? interface HashMap { [index: number]: string; } ...

I'm trying to figure out how to access the array field of an object in TypeScript. It seems like the type 'unknown' is required to have a '[Symbol.iterator]()' method that returns an iterator

I'm currently tackling an issue with my helper function that updates a form field based on the fieldname. For example, if it's the name field, then form.name will be updated. If it's user[0].name, then the name at index 0 of form.users will ...

The issue I'm facing with the mongoose schema.method is that the TypeScript error TS2339 is showing up, stating that the property 'myMethod' does not exist on type 'Model<MyModelI>'

I am looking to integrate mongoose with TypeScript and also want to enhance Model functionality by adding a new method. However, when I try to transpile the file using tsc, I encounter the following error: spec/db/models/match/matchModelSpec.ts(47,36): e ...

Guide on sending files and data simultaneously from Angular to .NET Core

I'm currently working on an Angular 9 application and I am trying to incorporate a file upload feature. The user needs to input title, description, and upload only one file in .zip format. Upon clicking Submit, I intend to send the form data along wit ...

The export enumeration in Typescript-Angular is not defined

I've encountered a strange issue in my Angular project. I have some components and enums set up, and everything was working fine with one component using the enums. But when I tried to implement the same enums in other components, they are returning " ...

An object in typescript has the potential to be undefined

Just starting out with Typescript and hitting a snag. Can't seem to resolve this error and struggling to find the right solution useAudio.tsx import { useEffect, useRef } from 'react'; type Options = { volume: number; playbackRate: num ...

Encountering an Issue with Dynamic Imports in Cypress Tests Using Typescript: Error Loading Chunk 1

I've been experimenting with dynamic imports in my Cypress tests, for example using inputModule = await import('../../__tests__/testCases/baseInput'); However, I encountered an issue with the following error message: ChunkLoadError: Loading ...

Tailor TypeScript to support various JavaScript versions

One of the advantages of TypeScript is the ability to target different versions of Javascript globally - allowing for seamless switching between transpiling ES3, ES5, or ES6. For browsers like IE that require ES3 support, it serves as the lowest common de ...

Is it feasible to differentiate generic argument as void in Typescript?

One of the functions in my code has a generic type argument. In certain cases, when the context is void, I need to input 0 arguments; otherwise, I need to input 1 argument. If I define the function argument as context: Context | void, I can still add voi ...

Is there a way to programmatically activate the iOS unavailable screen?

Is there a way to programmatically simulate the iPhone unavailable screen after entering the wrong password multiple times, with a specific time delay? I am searching for an API that can remotely lock my iPhone screen so that it cannot be unlocked by me. ...