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
?
CodePudding user response:
C[S]
type is getting from App
. You don't need to use extends
but itself directly in the returned params
We also cannot predict the function's returned type, so I'd use
ReturnType<(...args: any[]) => any>
The full possible change is below
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] | ReturnType<(...args: any[]) => any> {
const method = context[prop];
if (typeof method === 'function') {
return method(); //TS knows method `name` returns a string but not C[S]
} else {
return method; //TS knows this as 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
You can verify it with this playground
If you can predict the returned types from name
, you can follow the below types passing
C[S] | ReturnType<() => string>
Or
C[S] | string
CodePudding user response:
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