By millimoose


2019-12-02 22:03:25 8 Comments

I'm trying to make a generic type "get property using string key or callback" function and I've hit a wall getting TS to narrow my type parameter to a key of the containing object.

The function is as follows:

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');
    }
}

And the compiler complains in the string branch - apparently prop gets narrowed not to P, but to P & string, which can't be used to here, because it implies trying to return T[string] where a T[P] is required.

Is there a way to specify this correctly, or do I just sigh and suppress the error?

2 comments

@Maciej Sikora 2019-12-02 22:25:57

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)) {
    if (typeof prop === 'function') {
        return prop(value);
    }
    if (prop in value) {
      return value[prop];
    }
    throw new TypeError('Property getter must be string or function');
}

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

Explanation. There were few problems in the previous implementation:

  • return type is already defined in overloads, no need to define it again in the implementation
  • checking typeof string automatically creates an intersection with string type

The solution is to use key in object syntax, this is giving us type specification to V[P] and the second branch is checking typeof function


The exact problem with intersection with string is, that orginal implementation is not able to work with all possible keys types, which are - number | string | symbol. For anything other then string as key, the function will throw exception. Consider below example:

  // symbol prop example
  const symbolProp = Symbol()
  const v = get({[prop]: 'value'}, prop);
  // array example
  const v2 = get([1, 2], 1);
  // object with number key example
  const v3 = get({1: 'value'}, 1);

All three examples will be type correct, but will throw an error, as key is not a string. For the solution I am proposing, all of them will work correctly. The key difference is prop in value which ensures that prop is a key of value, but not require specific type of the key.


If really we want to ensure that we want only string keys, then the function type definition should be reflecting that. Consider:

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

Core difference is - P extends keyof T & string we are saying on the type level that we accept only keys of P, which are also strings. That approach is consistent with the implementation where we check typeof string.

@Patrick Roberts 2019-12-02 22:48:09

I'd suggest making the union signature function get<T, V, P extends keyof T>(value: T, prop: P | ((value: T) => V)), but otherwise I do like this answer better than the accepted one.

@millimoose 2019-12-04 07:20:50

Oh I remember, I didn’t go with this one because JS doesn’t throw when you’re accessing a property the object doesn’t have, and leaving properties unset is widely idiomatic. (And JSON can’t roundtrip a property whose value is undefined, so using this would break with any data that uses that instead of explicit null.) It’s at the hairsplitting level though and you make good points, I just don’t particularly need nonstring keys captured from function parameters very often; although for the sake of correctness I’ll add the constraint.

@Medet Tleukabiluly 2019-12-02 22:21:55

Don't need to double check

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 HERE
        default: throw new TypeError('Property getter must be string or function');
    }
}

Demo

@millimoose 2019-12-02 22:26:15

Hm. I mean the implementation signature then becomes a bed of lies, because when prop is a function it won't return the inferred retval of the function. But I suppose that's rather irrelevant since calls get checked against the overload signatures; and since the overload signatures aren't checked against the implementation they're a bed of lies by definition.

Related Questions

Sponsored Content

3 Answered Questions

[SOLVED] Typescript 'keyof' keyword makes error when using with react setState function

  • 2018-06-06 08:10:35
  • jeyongOh
  • 363 View
  • 0 Score
  • 3 Answer
  • Tags:   typescript

2 Answered Questions

1 Answered Questions

[SOLVED] How to infer the type of object keys from a string array

7 Answered Questions

[SOLVED] Are strongly-typed functions as parameters possible in TypeScript?

  • 2013-02-01 02:56:27
  • vcsjones
  • 238599 View
  • 493 Score
  • 7 Answer
  • Tags:   typescript

1 Answered Questions

[SOLVED] Using TS conditional types to narrow to numeric properties

  • 2019-05-13 20:50:57
  • joshstrike
  • 67 View
  • 1 Score
  • 1 Answer
  • Tags:   typescript

1 Answered Questions

[SOLVED] Correct type inference with keyof in generic functions

1 Answered Questions

[SOLVED] How to write omit function with proper types in typescript

1 Answered Questions

[SOLVED] How to narrow typescript array of string literal/string enum values

  • 2018-10-20 01:34:39
  • user1234
  • 397 View
  • 0 Score
  • 1 Answer
  • Tags:   typescript

2 Answered Questions

[SOLVED] Typescript: Infer lookup type for function parameter within object

  • 2018-08-15 10:51:03
  • robjtede
  • 269 View
  • 0 Score
  • 2 Answer
  • Tags:   typescript types

1 Answered Questions

[SOLVED] Use of Conditional types and mapped types with array reduce method

  • 2018-08-11 08:41:51
  • ford04
  • 102 View
  • 0 Score
  • 1 Answer
  • Tags:   typescript

Sponsored Content