映射对象并保留类型推断



我正试图创建一个函数,通过映射函数映射对象。

interface Dictionary<T> {
[key: string]: T;
}
function objectMap<TValue, TResult>(
obj: Dictionary<TValue>,
valSelector: (val: TValue) => TResult
) {
const ret = {} as Dictionary<TResult>;
for (const key of Object.keys(obj)) {
ret[key] = valSelector.call(null, obj[key]);
}

return ret;
}

然后像这样使用:

const myObj = {
withString: {
api: (id: string) => Promise.resolve(id),
},
withNumber: {
api: (id: number) => Promise.resolve(id),
},
}
const mapToAPI = objectMap(myObj, (val => val.api));
mapToAPI.withString('some string');

最后一行出现错误:

Argument of type 'string' is not assignable to parameter of type 'never'.

如何映射泛型对象并保留类型推断?

正如jcalz所指出的,这种任意变换在TS中是不可能的。

然而,如果你只是简单地利用这个功能来浏览树,那么有更好的方法可以实现

如果要将导航性保持为树,而不是平面对象,可以使用lodash中的一些帮助以及type-fest中的实用程序类型来键入返回值。

import _ from "lodash"
import { Get } from "type-fest"
function objectMap<
Dict extends Record<string, any>,
Path extends string
>(
obj: Dict,
path: Path
): {
[Key in keyof Dict]: Get<Dict, `${Key}.${Path}`>
} {
const ret = {} as Record<string, any>
for (const key of Object.keys(obj)) {
ret[key] = _.get(obj[key], path)
}
return ret as any
}

这样就可以工作了,不需要显式键入。

const myObj = {
withString: {
api: (id: string) => Promise.resolve(id),
wrong_api: (id: number) => Promise.resolve(id), //This is the same as withNumber.api
similiar_api: (some_id: string) => Promise.resolve(some_id),
remove_api: (some_id: boolean) => Promise.resolve(some_id),
helpers: {
help: (id: number) => {}
}
},
withNumber: {
api: (id: number) => Promise.resolve(id),
helpers: {
help: (id: number) => {}
},
not_an_api: false,
},
}
const mapToAPI = objectMap(myObj, 'api');
const mapToAPI = objectMap(myObj, 'helpers.help');

如果你想在路径上进行类型推理以确保只有有效的路径,你可以使用另一个实用函数,一个递归函数来将对象节点转换为所有字符串的并集,尽管这只是为处理对象而构建的,并且可能会在数组上爆炸。如果你有一个足够大的对象,它也会爆炸并抛出Type instantiation is excessively deep and possibly infinite,因此它最初没有被包括在内。

import { UnionToIntersection } from "type-fest";
type UnionForAny<T> = T extends never ? 'A' : 'B';
type IsStrictlyAny<T> = UnionToIntersection<UnionForAny<T>> extends never
? true
: false;
export type AcceptablePaths<Node, Stack extends string | number = ''> = 
IsStrictlyAny<Node> extends true ? `${Stack}` :
Node extends Function ? `${Stack}` :
Node extends Record<infer Key, infer Item> 
? Key extends number | string 
? Stack extends ''
? `${AcceptablePaths<Item, Key>}`
: `${Stack}.${AcceptablePaths<Item, Key>}`
: ''
: ''

最后,像这个

function objectMap<
Dict extends Record<string, any>,
Path extends AcceptablePaths<Dict[keyof Dict]>,
>(
obj: Dict,
path: Path
): 
{
[Key in keyof Dict]: Get<Dict, `${Key}.${Path}`>
} {/** Implementation from above **/}

在代码沙盒上查看

以下内容主要基于@kellys的注释,如果存在其他属性,则要求类型K显式,但它似乎有效:

function objectMap<T, K extends keyof T[keyof T]>(
obj: T,
valSelector: (o: T[keyof T]) => T[keyof T][K],
): {
[P in keyof T]: T[P][K];
} {
const ret = {} as {
[P in keyof T]: T[P][K];
};
for (const key of Object.keys(obj) as (keyof T)[]) {
ret[key] = valSelector(obj[key]);
}

return ret;
}
const myObj = {
withString: {
api: (id: string) => Promise.resolve(id),
bea: "ciao",
},
withNumber: {
api: (id: number) => Promise.resolve(id),
bea: 123
},
}
const mapToAPI = objectMap<typeof myObj, "api">(myObj, val => val.api);
mapToAPI.withString('some string');
mapToAPI.withNumber(123)

游乐场

也许有人能够进一步改进它。

最新更新