TypeScript doesn't natively support the specific type of higher-order generic function manipulation you're looking for in this scenario. It would be ideal to specify "use T
in typeof retrieve
with User
", but unfortunately, there isn't a straightforward way to achieve this within the language. Perhaps if TypeScript included features like higher-kinded types or generic values, as requested in microsoft/TypeScript#1213 or microsoft/TypeScript#17574, this could be done more smoothly.
To work around this limitation without being ad-hoc, you can explicitly define the type "specify T
in typeof retrieve
with User
", annotate a variable with that type and assign retrieve
to it:
const userRetrieve: (endpoint: string, id: string, /* etc */) => User = retrieve; // okay
The compiler accepts this approach, indicating that although programmatically specifying T
with
User</code in the type system may not be easy, it recognizes that <code>retrieve
is compatible with the intended type. This maintains type safety by flagging any errors if the type is incorrect.
Subsequently, you can bind a parameter to userRetrieve
:
const retrieveUser = userRetrieve.bind(undefined, "/user");
// const retrieveUser: (id: string) => User
If the parameters of retrieve
are non-generic, you can utilize the Parameters<T>
utility type to streamline the process:
const userRetrieve: (...args: Parameters<typeof retrieve>) => User = retrieve; // okay
const retrieveUser = userRetrieve.bind(undefined, "/user");
// const retrieveUser: (id: string) => User
In situations where expedience outweighs strict type safety, you can replace the annotated variable with a type assertion:
const retrieveUser =
(retrieve as (...args: Parameters<typeof retrieve>) => User).
bind(undefined, "/user");
This concise method sacrifices some level of type safety for convenience.
If the parameters rely on the generic type parameter, using Parameters<T>
will not retain track of any type parameter:
declare function retrieve<T>(endpoint: string, id: string, somethingGeneric: T): T;
const retrieveUser =
(retrieve as (...args: Parameters<typeof retrieve>) => User).
bind(undefined, "/user");
// const retrieveUser: (id: string, somethingGeneric: unknown) => User
In this case, where the type is mistakenly inferred as unknown
instead of
User</code, manual intervention is necessary to rectify this discrepancy:</p>
<pre><code>const retrieveUser =
(retrieve as (endpoint: string, id: string, somethingGeneric: User) => User).
bind(undefined, "/user");
// const retrieveUser: (id: string, somethingGeneric: User) => User
While there are ways to leverage limited support for higher order generic function inference to automate inserting T
, it's quite complex and esoteric. For further insights into this method, refer to this answer.
class GenTypeMaker<T> {
getGenType!: <A extends any[], R>(cb: (...a: A) => R) => () => (...a: A) => R;
genType = this.getGenType(null! as typeof retrieve)<T>()
}
type Retrieve<T> = GenTypeMaker<T>['genType']
// type Retrieve<T> = (endpoint: string, id: string, somethingGeneric: T) => T
With careful specification, the type for Retrieve<T>
aligns with the function type of retrieve</code when <code>T
is designated. Thus, Retrieve<User>
encompasses the desired type:
const retrieveUser =
(retrieve as Retrieve<User>).
bind(/*this*/ undefined, "/user");
// const retrieveUser: (id: string, somethingGeneric: User) => User
This technique is intriguing but not recommended for everyday use. In cases like these, it's advisable to manually declare and annotate the appropriate types for clarity and accuracy.
Playground link to code