Get function result or value from an object by property

671 Views Asked by At

TS Playground

I'm trying to figure out the correct type for a utility function getProp that will index an object by key and return the primitive value at that index or the result of the function at that index.

declare interface App {
  name: () => string
  version: string
  id: number
}

const getProp = function<C extends App, S extends keyof App>(context: C, prop: S) : C[S] extends (...args: any[]) => any ? ReturnType<C[S]> : C[S] {
  const method = context[prop];
  if (typeof method === 'function') {
    return method(); // Error: Type 'string' is not assignable to type 'C[S] extends (...args: any[]) => any ? ReturnType<C[S]> : C[S]'.
  } else {
    return method; // Error: Type '() => string' is not assignable to type 'C[S] extends (...args: any[]) => any ? ReturnType<C[S]> : C[S]'.
  }
};

declare var app : App;

// These are all correct despite the errors
getProp(app, "id") // => number
getProp(app, "name") // => string
getProp(app, "version") // => string

The type presented here "works" but TypeScript is giving me errors on the lines indicated. Is there a better way to type this properly without errors or should I just drop in my old friend //@ts-ignore?

1

There are 1 best solutions below

5
On BEST ANSWER

When we use Generic Functions the actual type instantiation happens when we invoke it, getProp<App, 'name'>

Hence the Typescript compiler needs to be sure that the types we are using must be compatible and follow the constraints.

Since the Return Type of getProp is conditional we can make use of assertion/aliasing to inform the typescript compiler of the state in which this branch will be reached

In your code, the compiler was complaining that it is possible that you are returning some code (which might not match the return value) at the time of declaration!

Hence by aliasing it, we make sure that the types will be of this ReturnType only!

To avoid Repetition I created a new type alias PropReturnType<T>

declare interface App {
  name: () => string
  version: string
  id: number
}

type PropReturnType<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? ReturnType<T[K]> : T[K] }


const getProp = function<C extends App, S extends keyof C>(context: C, prop: S): PropReturnType<C>[S]{
  const method = context[prop];
  if (typeof method === 'function') {
    return method();
  } else {
    return method as PropReturnType<C>[S];
  }
};



declare var app: App;

getProp(app, "id") // => number
getProp(app, "name") // => string
getProp(app, "version") // => string

CODE PLAYGROUND