将函数创建者的对象映射到创建函数的对象时如何维护类型推断?
How to maintain type inference when mapping an object of function creators to an object of created functions?
TLDR: 我正在尝试创建一个映射器函数,它将 HttpClient“应用”到函数创建者,并维护所创建函数的类型信息。 input/output 的键入示例:
retrieveAge: (id: string) => (client: HttpClient) => number;
modifyName: (id: string, newName: string) => (client: HttpClient) => void;
->
retrieveAge: (id: string) => number;
modifyName: (id: string, name: string) => void;
结果是创建的函数(不再需要使用 HttpClient 调用)。
Context/Attempt
我正在建立一个 API 方法调用 get/post 数据的库。我想公开允许消费者传入他们自己的“http 客户端”的方法,但也公开已经“附加”了默认 http 客户端的相同方法。我想确保导出方法的两个版本都保留输入信息。
我正在尝试的方法(也许这不是最好的方法)是使 retrieve/update 方法函数创建器,您可以使用 http 客户端调用这些函数创建器,以使用您希望的客户端启动它们使用(默认或其他方式)。
我有一个半可行的解决方案,但它只有在所有方法都遵循确切的接口时才有效。否则,它会抱怨传入对象中方法的调用签名不匹配。例如,它抱怨 retrieveAge 中的类型 number 不能分配给 retrieveName 中的类型 Name。
Type 'number' is not assignable to type 'Name'.
如果我的方法对象中只有一个方法,它工作正常。
我在下面用裸 JS 和 TS 重现了这个问题。在底部,您可以看到我手动创建了我希望“applyClient”函数创建的对象,以及生成的类型。
Link 到 playground 使用相同的代码。
interface Name {
firstName: string,
lastName: string
}
type Age = number;
interface HttpClient {
get<T>(resource: string): T,
post<T>(resource: string, payload: T): void
}
// fake "default" client
const defaultClient: HttpClient = {
get: function<T>(){return '123' as unknown as T},
post: function<T>(payload: T){ }
}
const methods = {
retrieveAge: (id: string) => (client: HttpClient) => client.get<Name>(`name/${id}`),
retrieveName: (id: string) => (client: HttpClient) => client.get<Age>(`age/${id}`),
modifyName: (id: string, newName: string) => (client: HttpClient) => client.post<string>(`name/${id}`, newName)
}
/** TYPES
* const methods: {
retrieveAge: (id: string) => (client: HttpClient) => number;
retrieveName: (id: string) => (client: HttpClient) => Name;
modifyName: (id: string, newName: string) => (client: HttpClient) => void;
}
*/
methods.retrieveAge('34442')(defaultClient);
methods.retrieveName('34442')(defaultClient);
methods.modifyName('34442', 'John Smith')(defaultClient);
type HttpMethod<T, K> = (...args: T[]) => (client: HttpClient) => K;
type Method<T, K> = (...args: T[]) => K;
function applyClient<T, K>(obj: Record<string, HttpMethod<T, K>>, client: HttpClient){
const mappedObject: Record<keyof typeof obj, Method<T, K>> = {};
for (let [key, method] of Object.entries(obj)) {
mappedObject[key] = (...args) => method(...args)(client);
}
return mappedObject;
}
const newMethods = applyClient(methods, defaultClient);
newMethods.retrieveAge('34442');
newMethods.retrieveName('34442');
newMethods.modifyName('34442', 'John Smith');
// This is the object/typings that I was hoping the applyClient method would produce:
// const convertedMethods = {
// retrieveAge: (id: string) => methods.retrieveAge(id)(client),
// retrieveName: (id: string) => methods.retrieveName(id)(client),
// modifyName: (id: string, name: string) => methods.modifyName(id, name)(client)
// }
/** TYPES
* const convertedMethods: {
retrieveAge: (id: string) => number;
retrieveName: (id: string) => Name;
modifyName: (id: string, name: string) => void;
}
*/
// This is how the methods would be called after having the client "applied" already:
// convertedMethods.retrieveAge('34442');
// convertedMethods.retrieveName('34442');
// convertedMethods.modifyName('34442', 'John Smith');
由于 TypeScript 中的映射类型,您想要实现的目标是可能的。
我提供了一些可以正常工作的代码,除了它利用 Axios 而不是您的自定义 HTTPClient 之外。我在代码中包含了注释,应该详细解释所有内容。您可以跳到答案的底部以查看示例用法和结果,然后 return 到开头以了解实施细节。原始return类型和参数类型信息成功保留。
首先,外函数和内函数的类型定义,外函数的参数类型,以及一些return类型定义:
// Requestor is the inner function, which expects to retrieve an axios instance and return some value of some type
type Requestor<R> = (axios: AxiosInstance) => R;
/*
Creator is the outer function, which returns a Requestor.
Note the two generic arguments that Creator takes: Arguments and R.
Arguments: provides typing for the arguments of the Creator (examples below).
R: The return type of the Creator, which is also the return type of the inner Requestor function
These two generic arguments maximize reusability of the Creator type.
*/
type Creator<Arguments extends [...args: any], R> = (...args: Arguments) => Requestor<R>;
/*
GetRequestArgs is an example of what could be passed to an axios.get() call.
This type defines an arguments list of length 1. The argument name is 'config' and its type is AxiosRequestConfig
Note: this example assumes that the data property of the AxiosRequestConfig is not utilized.
*/
type GetRequestArgs = [config?: AxiosRequestConfig];
/*
DataRequestArgs is a compound Arguments type definition. The first argument has name 'data'
and can have any type you'd like (hence the generic argument 'D'). The second argument.
NOTE: The name 'getRequestArgs' can be thought of as a placeholder. ...getRequestArgs expands
the GetRequestArgs Arguments definition into this new definition. Therefore, the resulting
arguments definition is actually: data: D, config?: AxiosRequestConfig
*/
type DataRequestArgs<D> = [data: D, ...getRequestArgs: GetRequestArgs];
/*
Another example of a compound Arguments definition. The result of this definition is:
customArg: string, otherArg: number, data: D, config: AxiosRequestConfig
See above note regarding argument expansion.
*/
type CustomArgs<D> = [customArg: string, otherArg: number, ...dataArgs: DataRequestArgs<D>];
// some simple return type definitions
type FirstReturnType = {value: string};
type SecondReturnType = {value: string, errorCode: number};
接下来,定义对象的“魔法”类型定义是应用默认 Axios 实例的结果:
/*
The magic. This type defines the new object that is the result of iterating over an object whose values
are Creators and applying an axios instance to each of them.
Note: line breaks added in order to easily add explanatory comments. The actual type definition
could be defined in a single line.
*/
// T here is the mapping described above
type Mapped<T> = {
// Set constrains on the keys of the new object. We want the keys of the original mapping to be preserved.
[K in keyof T]
// anything after this colon marks the start of the type definition of our new object.
:
// The beginning of a ternary expression. T[K] represents a Creator since T is our original mapping
// and K represents a key of the aforementioned mapping. Here, we check if the result of accessing our
// mapping using key K results in something that can be invoked.
T[K] extends (...args: any) => any ?
// The truthy part of the ternary expression. Here, we are certain that T[K] can be invoked.
// Therefore, the value of our new object at each key K will be the function definition seen below.
// ...args: Parameters<T[K] allows us to keep the typing of the arguments of each Creator in our original mapping.
// ReturnType<ReturnType<T[K]>> allows us to keep the typing of the Requestor returned by each Creator.
(...args: Parameters<T[K]>) => ReturnType<ReturnType<T[K]>>
// T[K] was not actually something that can be invoked, oh well.
: never
};
接下来,应用默认axios实例的函数:
/*
Function that iterates over an object whose values are Creators and applies an axios instance to each of them.
The result of this function is a new object that has the keys of the original mapping. The values of the new object
are functions that maintain the same Arguments typing information of the original Creators, but with a default
axios instance applied. The return type information of the original Creators is also maintained.
*/
function prepCreators<T>(creators: T): Mapped<T>{
let returnValue = {};
for(let [name, creator] of Object.entries(creators)){
returnValue[name] = (…args: any) => creator(…args)(axios);
}
return returnValue as Mapped<T>;
}
接下来,一些示例 Creator 定义:
/*
First example of a Creator definition. This creator is expected to return a Promise of type FirstReturnType.
Note how we do not have to provide typing information for the config argument. The typing information has
already been provided by the GetRequestArgs type. Therefore, the config argument is guaranteed to be of type
AxiosRequestConfig.
The config argument is optional.
*/
const myFirstCreator: Creator<GetRequestArgs, Promise<FirstReturnType>> = function (config){
return (axios) => {
return axios.get("https://myurl.com/path", config).then(()=> {
// Note how we are essentially returning an object of type FirstReturnType
return {value: 'val'};
});
}
}
/*
Second Creator example that has a return type of Promise<SecondReturnType>. Note how the Arguments definition
is now of type DataRequestArgs<string>. This means that the data argument must be a string. The config argument
must be of type AxiosRequestConfig. Again, we do not have to provide typing information within the arguments list
because the typing information is derived from DataRequestArgs<string>.
The config argument is optional.
*/
const mySecondCreator: Creator<DataRequestArgs<string>, Promise<SecondReturnType>> = function (data, config){
return (axios) => {
return axios.post("https://myurl.com/path", data, config).then(()=> {
return {value: 'val', errorCode: 1004};
});
}
}
/*
Third Creator example that has a return type of Promise<string>. The Arguments typing in this case
is provided by CustomArgs<string>. This means that the customArg argument is of type string and otherArg
is a number. The data argument is of type string, and config is still of type AxiosRequestConfig.
The config argument is optional.
*/
const myThirdCreator: Creator<CustomArgs<string>, Promise<string>> = function (customArg, otherArg, data, config){
// do something with customArg and otherArg here
return (axios) => {
return axios.post("https://myurl.com/path", data, config).then(()=> {
return "string";
});
}
}
包含我们最初创作者的原始映射:
// An object containing our creators.
let creators = {
myFirstCreator, mySecondCreator, myThirdCreator
};
示例用法和结果:
// transform our creators (apply a default axios instance to them).
let myClient = prepCreators(creators);
/*
Call examples (note how the config argument is optional).
*/
// retVal is a Promise<FirstReturnType>. 'value' is of type FirstReturnType
const retVal = myClient.myFirstCreator().then((value) => value);
// retVal2 is a Promise<SecondReturnType>. 'value' is of type SecondReturnType
const retVal2 = myClient.mySecondCreator('string data').then((value) => value);
// retVal3 is a Promise<string>. 'value' is of type string
const retVal3 = myClient.myThirdCreator('customArg', 5, 'dataArg').then((value)=> value);
TLDR: 我正在尝试创建一个映射器函数,它将 HttpClient“应用”到函数创建者,并维护所创建函数的类型信息。 input/output 的键入示例:
retrieveAge: (id: string) => (client: HttpClient) => number;
modifyName: (id: string, newName: string) => (client: HttpClient) => void;
->
retrieveAge: (id: string) => number;
modifyName: (id: string, name: string) => void;
结果是创建的函数(不再需要使用 HttpClient 调用)。
Context/Attempt
我正在建立一个 API 方法调用 get/post 数据的库。我想公开允许消费者传入他们自己的“http 客户端”的方法,但也公开已经“附加”了默认 http 客户端的相同方法。我想确保导出方法的两个版本都保留输入信息。
我正在尝试的方法(也许这不是最好的方法)是使 retrieve/update 方法函数创建器,您可以使用 http 客户端调用这些函数创建器,以使用您希望的客户端启动它们使用(默认或其他方式)。
我有一个半可行的解决方案,但它只有在所有方法都遵循确切的接口时才有效。否则,它会抱怨传入对象中方法的调用签名不匹配。例如,它抱怨 retrieveAge 中的类型 number 不能分配给 retrieveName 中的类型 Name。
Type 'number' is not assignable to type 'Name'.
如果我的方法对象中只有一个方法,它工作正常。
我在下面用裸 JS 和 TS 重现了这个问题。在底部,您可以看到我手动创建了我希望“applyClient”函数创建的对象,以及生成的类型。 Link 到 playground 使用相同的代码。
interface Name {
firstName: string,
lastName: string
}
type Age = number;
interface HttpClient {
get<T>(resource: string): T,
post<T>(resource: string, payload: T): void
}
// fake "default" client
const defaultClient: HttpClient = {
get: function<T>(){return '123' as unknown as T},
post: function<T>(payload: T){ }
}
const methods = {
retrieveAge: (id: string) => (client: HttpClient) => client.get<Name>(`name/${id}`),
retrieveName: (id: string) => (client: HttpClient) => client.get<Age>(`age/${id}`),
modifyName: (id: string, newName: string) => (client: HttpClient) => client.post<string>(`name/${id}`, newName)
}
/** TYPES
* const methods: {
retrieveAge: (id: string) => (client: HttpClient) => number;
retrieveName: (id: string) => (client: HttpClient) => Name;
modifyName: (id: string, newName: string) => (client: HttpClient) => void;
}
*/
methods.retrieveAge('34442')(defaultClient);
methods.retrieveName('34442')(defaultClient);
methods.modifyName('34442', 'John Smith')(defaultClient);
type HttpMethod<T, K> = (...args: T[]) => (client: HttpClient) => K;
type Method<T, K> = (...args: T[]) => K;
function applyClient<T, K>(obj: Record<string, HttpMethod<T, K>>, client: HttpClient){
const mappedObject: Record<keyof typeof obj, Method<T, K>> = {};
for (let [key, method] of Object.entries(obj)) {
mappedObject[key] = (...args) => method(...args)(client);
}
return mappedObject;
}
const newMethods = applyClient(methods, defaultClient);
newMethods.retrieveAge('34442');
newMethods.retrieveName('34442');
newMethods.modifyName('34442', 'John Smith');
// This is the object/typings that I was hoping the applyClient method would produce:
// const convertedMethods = {
// retrieveAge: (id: string) => methods.retrieveAge(id)(client),
// retrieveName: (id: string) => methods.retrieveName(id)(client),
// modifyName: (id: string, name: string) => methods.modifyName(id, name)(client)
// }
/** TYPES
* const convertedMethods: {
retrieveAge: (id: string) => number;
retrieveName: (id: string) => Name;
modifyName: (id: string, name: string) => void;
}
*/
// This is how the methods would be called after having the client "applied" already:
// convertedMethods.retrieveAge('34442');
// convertedMethods.retrieveName('34442');
// convertedMethods.modifyName('34442', 'John Smith');
由于 TypeScript 中的映射类型,您想要实现的目标是可能的。
我提供了一些可以正常工作的代码,除了它利用 Axios 而不是您的自定义 HTTPClient 之外。我在代码中包含了注释,应该详细解释所有内容。您可以跳到答案的底部以查看示例用法和结果,然后 return 到开头以了解实施细节。原始return类型和参数类型信息成功保留。
首先,外函数和内函数的类型定义,外函数的参数类型,以及一些return类型定义:
// Requestor is the inner function, which expects to retrieve an axios instance and return some value of some type
type Requestor<R> = (axios: AxiosInstance) => R;
/*
Creator is the outer function, which returns a Requestor.
Note the two generic arguments that Creator takes: Arguments and R.
Arguments: provides typing for the arguments of the Creator (examples below).
R: The return type of the Creator, which is also the return type of the inner Requestor function
These two generic arguments maximize reusability of the Creator type.
*/
type Creator<Arguments extends [...args: any], R> = (...args: Arguments) => Requestor<R>;
/*
GetRequestArgs is an example of what could be passed to an axios.get() call.
This type defines an arguments list of length 1. The argument name is 'config' and its type is AxiosRequestConfig
Note: this example assumes that the data property of the AxiosRequestConfig is not utilized.
*/
type GetRequestArgs = [config?: AxiosRequestConfig];
/*
DataRequestArgs is a compound Arguments type definition. The first argument has name 'data'
and can have any type you'd like (hence the generic argument 'D'). The second argument.
NOTE: The name 'getRequestArgs' can be thought of as a placeholder. ...getRequestArgs expands
the GetRequestArgs Arguments definition into this new definition. Therefore, the resulting
arguments definition is actually: data: D, config?: AxiosRequestConfig
*/
type DataRequestArgs<D> = [data: D, ...getRequestArgs: GetRequestArgs];
/*
Another example of a compound Arguments definition. The result of this definition is:
customArg: string, otherArg: number, data: D, config: AxiosRequestConfig
See above note regarding argument expansion.
*/
type CustomArgs<D> = [customArg: string, otherArg: number, ...dataArgs: DataRequestArgs<D>];
// some simple return type definitions
type FirstReturnType = {value: string};
type SecondReturnType = {value: string, errorCode: number};
接下来,定义对象的“魔法”类型定义是应用默认 Axios 实例的结果:
/*
The magic. This type defines the new object that is the result of iterating over an object whose values
are Creators and applying an axios instance to each of them.
Note: line breaks added in order to easily add explanatory comments. The actual type definition
could be defined in a single line.
*/
// T here is the mapping described above
type Mapped<T> = {
// Set constrains on the keys of the new object. We want the keys of the original mapping to be preserved.
[K in keyof T]
// anything after this colon marks the start of the type definition of our new object.
:
// The beginning of a ternary expression. T[K] represents a Creator since T is our original mapping
// and K represents a key of the aforementioned mapping. Here, we check if the result of accessing our
// mapping using key K results in something that can be invoked.
T[K] extends (...args: any) => any ?
// The truthy part of the ternary expression. Here, we are certain that T[K] can be invoked.
// Therefore, the value of our new object at each key K will be the function definition seen below.
// ...args: Parameters<T[K] allows us to keep the typing of the arguments of each Creator in our original mapping.
// ReturnType<ReturnType<T[K]>> allows us to keep the typing of the Requestor returned by each Creator.
(...args: Parameters<T[K]>) => ReturnType<ReturnType<T[K]>>
// T[K] was not actually something that can be invoked, oh well.
: never
};
接下来,应用默认axios实例的函数:
/*
Function that iterates over an object whose values are Creators and applies an axios instance to each of them.
The result of this function is a new object that has the keys of the original mapping. The values of the new object
are functions that maintain the same Arguments typing information of the original Creators, but with a default
axios instance applied. The return type information of the original Creators is also maintained.
*/
function prepCreators<T>(creators: T): Mapped<T>{
let returnValue = {};
for(let [name, creator] of Object.entries(creators)){
returnValue[name] = (…args: any) => creator(…args)(axios);
}
return returnValue as Mapped<T>;
}
接下来,一些示例 Creator 定义:
/*
First example of a Creator definition. This creator is expected to return a Promise of type FirstReturnType.
Note how we do not have to provide typing information for the config argument. The typing information has
already been provided by the GetRequestArgs type. Therefore, the config argument is guaranteed to be of type
AxiosRequestConfig.
The config argument is optional.
*/
const myFirstCreator: Creator<GetRequestArgs, Promise<FirstReturnType>> = function (config){
return (axios) => {
return axios.get("https://myurl.com/path", config).then(()=> {
// Note how we are essentially returning an object of type FirstReturnType
return {value: 'val'};
});
}
}
/*
Second Creator example that has a return type of Promise<SecondReturnType>. Note how the Arguments definition
is now of type DataRequestArgs<string>. This means that the data argument must be a string. The config argument
must be of type AxiosRequestConfig. Again, we do not have to provide typing information within the arguments list
because the typing information is derived from DataRequestArgs<string>.
The config argument is optional.
*/
const mySecondCreator: Creator<DataRequestArgs<string>, Promise<SecondReturnType>> = function (data, config){
return (axios) => {
return axios.post("https://myurl.com/path", data, config).then(()=> {
return {value: 'val', errorCode: 1004};
});
}
}
/*
Third Creator example that has a return type of Promise<string>. The Arguments typing in this case
is provided by CustomArgs<string>. This means that the customArg argument is of type string and otherArg
is a number. The data argument is of type string, and config is still of type AxiosRequestConfig.
The config argument is optional.
*/
const myThirdCreator: Creator<CustomArgs<string>, Promise<string>> = function (customArg, otherArg, data, config){
// do something with customArg and otherArg here
return (axios) => {
return axios.post("https://myurl.com/path", data, config).then(()=> {
return "string";
});
}
}
包含我们最初创作者的原始映射:
// An object containing our creators.
let creators = {
myFirstCreator, mySecondCreator, myThirdCreator
};
示例用法和结果:
// transform our creators (apply a default axios instance to them).
let myClient = prepCreators(creators);
/*
Call examples (note how the config argument is optional).
*/
// retVal is a Promise<FirstReturnType>. 'value' is of type FirstReturnType
const retVal = myClient.myFirstCreator().then((value) => value);
// retVal2 is a Promise<SecondReturnType>. 'value' is of type SecondReturnType
const retVal2 = myClient.mySecondCreator('string data').then((value) => value);
// retVal3 is a Promise<string>. 'value' is of type string
const retVal3 = myClient.myThirdCreator('customArg', 5, 'dataArg').then((value)=> value);