Tips for properly narrowing a function parameter that includes "an object key or a function"

Working on a function to retrieve a property using either a string key or a callback, I've encountered an issue with TypeScript narrowing the type parameter. Here is the function in question:

function get<T, V>(value: T, fn: (value: T) => V): V;
function get<T, P extends keyof T>(value: T, prop: P): T[P];
function get<T, P extends keyof T>(value: T, prop: P | ((value: T) => any)):
    typeof prop extends (o: any) => infer V ? V : T[P] {
    switch (typeof prop) {
        case 'function':
            return prop(value);
        case 'string':
            return value[prop]; // ERROR HERE
        default: throw new TypeError('Property getter must be string or function');
    }
}

In the code snippet above, the issue arises in the branch where prop is of type string. This results in prop being narrowed down to P & string, which causes a mismatch as it should be T[P] instead of T[string].

Is there a better way to specify this behavior correctly, or should I simply ignore the error for now?

Answer №1

function fetch<T, V>(data: T, callback: (data: T) => V): V;
function fetch<T, K extends keyof T>(data: T, key: K): T[K];
function fetch<T, K extends keyof T>(data: T, key: K | ((data: T) => any)) {
    if (typeof key === 'function') {
        return key(data);
    }
    if (key in data) {
      return data[key];
    }
    throw new TypeError('Key getter must be a string or function');
}

const resultA = fetch(1, (a) => a + 1);
const resultB = fetch({a: 'a'}, 'a')
console.log(resultA); // 2
console.log(resultB);// "a"

Clarification: In the previous version of the code, there were some issues that needed to be addressed:

  • The return type was already defined in the overloads, so it didn't need to be repeated in the implementation.
  • Checking typeof string automatically creates an intersection with the string type.

The solution involved using the key in object syntax, which specifies the type as V[P], and the second branch checks for typeof function.


The specific problem with the intersection of string types is that the original implementation couldn't handle all possible types of keys, such as number | string | symbol. If a key other than a string is used, the function will throw an exception. For example:

// Example with a symbol key
const symbolKey = Symbol()
const value = fetch({[symbolKey]: 'value'}, symbolKey);
// Example with an array key
const value2 = fetch([1, 2], 1);
// Example with an object containing a number key
const value3 = fetch({1: 'value'}, 1);

All three examples would be correct in terms of type, but would throw an error because the key is not a string. The proposed solution allows all these examples to work correctly by using key in object to ensure the key is present in the object without requiring a specific key type.


If we specifically want to allow only string keys, the function type definition should reflect that. For example:

function fetch<T, V>(data: T, callback: (data: T) => V): V;
function fetch<T, K extends keyof T & string>(data: T, key: K): T[K];
function fetch<T, K extends keyof T & string>(data: T, key: K | ((data: T) => any)) {
    switch (typeof key) {
        case 'function':
            return key(data);
        case 'string':
            return data[key];
        default: throw new TypeError('Key getter must be a string or function');
    }
}

The key difference here is K extends keyof T & string, which specifies that we accept only keys of type K that are also strings. This approach aligns with the implementation where we check for typeof string.

Answer №2

No need for re-verification

function get<T, P extends keyof T>(value: T, prop: P | ((value: T) => any)):
    T[P] {
    switch (typeof prop) {
        case 'function':
            return prop(value);
        case 'string':
            return value[prop]; // ERROR OCCURS HERE
        default: throw new TypeError('Property getter must be string or function');
    }
}

View Demo

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

Using Angular2, assign a value to the session and retrieve a value from the session

I am having trouble getting and setting a session. Here is my code: login_btnClick() { var NTLoginID = ((document.getElementById("NTLoginID") as HTMLInputElement).value); this._homeService.get(Global.BASE_USER_ENDPOINT + '/EmployeeDe ...

React Type Mutation response feedback is a valuable tool for receiving input

I am facing an issue with passing the mutation success response in my code. I have a file named change-email.tsx which calls a component file updateEmail.tsx containing a mutation function. The submit function is working fine, but I cannot figure out how t ...

Ways to improve the feedback for Typescript when dealing with the potential existence of a nested method

Recently encountered a critical bug that I believe could have been identified with the right TypeScript setup. Struggling to come up with a suitable title, so please bear with me. While initializing a widget app, similar to a chat app loaded by a parent a ...

Unit Testing in Vue.JS: The original function remains active even after using sinon.stub()

While unit testing my components (which are coded using TypeScript along with vue-class-component), I am utilizing Sinon to stub API calls. However, even after adding the stub to the test, the original method is still being invoked instead of returning the ...

Is TypeScript to blame for the unexpected token error in Nock?

My code snippet in the ts file looks like this: nock('https://example.test').post('/submit').reply(200,{ "status": "Invalid", "message": "Invalid Request", }); However, when I try to ...

Switch back and forth between two pages within the same tab on an Ionic 3 app depending on the user's login information

I am seeking a way to switch between two profile pages using the profile tab based on whether the user is a student or tutor in an ionic 3 application. ...

Master the art of iterating through an Object in TypeScript

I need help with looping through an Object in TypeScript. While the following examples work in JavaScript, I understand why they pose a problem in TypeScript. Unfortunately, I am struggling to find the correct approach to solve this issue. Am I approaching ...

Using TypeScript to incorporate JS web assembly into your project

I have been attempting to incorporate wasm-clingo into my TypeScript React project. I tried creating my own .d.ts file for the project: // wasm-clingo.d.ts declare module 'wasm-clingo' { export const Module: any; } and importing it like this: ...

Mapping an array of objects using dynamically generated column names

If I have an array of objects containing country, state, city data, how can I utilize the .map method to retrieve unique countries, states, or cities based on specific criteria? How would I create a method that accepts a column name and maps it to return ...

Tips for transforming a Json array into an object in Angular 5

I am working with a Json array that looks like this: [{"name":"ip","children":{"label":"ip","value":"","type":"text","validation":"{ required: true}"}} ,{"name":"test","children":{"label":"test","value":"","type":"text","validation":"{ required: true}"}} ...

Prisma atomic operations encounter errors when attempting to update undefined values

According to the Prisma Typescript definition for atomic operations, we have: export type IntFieldUpdateOperationsInput = { set?: number increment?: number decrement?: number multiply?: number divide?: number } Let's take a look at the Pris ...

Using CreateMany within a Prisma Create query

Hello, I have been working on implementing a create statement that accepts an array of another model. Below are my schemas: model House { id String @id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt property_name String ...

An error occurred as the Serverless Function timed out while attempting to load a dynamic route in Next.js version 13

I have a basic Next.js application with the following route structure: /contentA/ - Static - Initial load: 103 kB /contentA/[paramA]/groups - SSG - Initial load: 120 kB /contentB/[paramA]/[paramB]/[paramC] - SSR (client component) - Initial load: 103 kB ...

Converting an Array of Objects into a single Object in React: A Tutorial

AccessData fetching information from the database using graphql { id: '', name: '', regions: [ { id: '', name: '', districts: [ { id: '', ...

The useEffect hook in React is signaling a missing dependency issue

Any tips on how to resolve warnings such as this one src\components\pages\badge\BadgeScreen.tsx Line 87:6: React Hook useEffect has a missing dependency: 'loadData'. Either include it or remove the dependency array react-hoo ...

The combination of React, Typescript, and MaterialUI Paper results in a stunning design with a sleek and

Whenever I include <Paper elevation={0} /> within my react component, I encounter the following issue: Cannot read properties of undefined (reading 'background') TypeError: Cannot read properties of undefined (reading 'background&apos ...

Is there a way to simultaneously call two APIs and then immediately call a third one using RXJS?

I am looking to optimize the process of making API calls by running two in parallel and then a third immediately after both have completed. I have successfully implemented parallel API calls using mergeMap and consecutive calls using concatMap, but now I w ...

Testing NestJS Global ModulesExplore how to efficiently use NestJS global

Is it possible to seamlessly include all @Global modules into a TestModule without the need to manually import them like in the main application? Until now, I've had to remember to add each global module to the list of imports for my test: await Tes ...

pick only one option from each row

I am working on a feature where I have five rows with two checkboxes each generated using a loop and property binding. Currently, clicking on one checkbox selects all elements in the column. However, I want to achieve selection row-wise. Is there a method ...

Discovering the parameter unions in Typescript has revolutionized the way

My current interface features overloaded functions in a specific format: export interface IEvents { method(): boolean; on(name: 'eventName1', listener: (obj: SomeType) => void): void; on(name: 'eventName2', listener: (obj: Som ...