TypeScript 通用映射可变元组值到嵌套映射类型
TypeScript generic map variadic tuple values to nested mapped type
我正在尝试制作一个辅助函数,它接受像 JSON 这样的嵌套对象,并允许在任意深度深度复制嵌套值。我理解可变元组类型并且可以让它们工作以传递元组 - 但我不知道如何 'map' 它们到任意深度的嵌套 Picks (它甚至可能是不可能的)。这是我想出的最好的 - 但仍然限于需要为 GetNestedValue 创建尽可能多的重载,因为我愿意支持。我理解各种错误,我只是想不出任何方法来满足编译器并在 return 值上完成类型。
// K is arbitrary length how to express N accessors deep? in TS without a loop?
type GetNestedValue<K extends string[], O extends any> = O[K[0]][K[1]][K[2]];
function getNestedItem<Keys extends string[], Obj>(
obj: Obj, ...keys: readonly [...Keys]
): GetNestedValue<Keys, Obj> extends undefined ? undefined : GetNestedValue<Keys, Obj> {
let level: any = obj;
for (const key of keys) {
if (level !== undefined) {
level = level[key];
} else {
return;
}
}
// this will return deepClone(level);
return level;
}
const obj = {one: 1, two: {three: {four: 4}}};
// I'd prefer 'known' shapes of obj here block form entering invalid keys.
const a = getNestedItem(obj, 'one', 'two');
// here - when arbitrarily trying to grab stuff from unknown inputs - I don't want
// a warning, rather the user would just need to check `if (b !== undefined)`
const b = getNestedItem(obj as any, 'one', 'two');
link 至 playground
有趣的是,您偏爱“已知”类型而拒绝使用未知类型,到处都是一堆 anys。我对可变元组类型一无所知,但这里有 3 个示例
export const getDeepProp = <Obj>(obj: Obj, ...keys: readonly [keyof Obj]) => {
const send: Record<string, unknown> = {};
keys.forEach(i=> {
type current = typeof obj[typeof i];
send[i] = obj[i] as current;
});
return send;
};
export function deepClone <O> (values:O): O {
if (values == null || typeof values != "object") return values;
const copy: Record<string, unknown> = {};
for (const attr in values) {
if (values[attr]) {
copy[attr] = deepClone(values[attr]);
}
}
return copy as O;
}
export function name<O> (params:O, ...keys: string[]) {
const a: Record<string,unknown> = {};
keys.forEach(key => {
a[key as keyof O] = typeof params[key as keyof O]==="object" ?
name(params[key as keyof O], ...Object.keys(params[key as keyof O])) : params[key as keyof O];
});
return a as O;
}
我首先要说的是:虽然这是一个有趣的思想实验,但由于它需要大量的递归,我不推荐这样做。
它需要两种递归类型,一种类型用于获取从对象类型推断出的一组有效键,另一种类型用于访问 属性 给定那些经过验证的键。对于 TypeScript < 4.5,深度限制将是一个长度为 10 的元组。
验证:
// walk through the keys and validate as we recurse. If we reach an invalid
// key, we return the currently validated set along with a type hint
type ValidatedKeys<K extends readonly PropertyKey[], O, ValidKeys extends readonly PropertyKey[] = []> =
K extends readonly [infer Key, ...infer Rest]
// Excluding undefined to allow `a?.b?.c`
? Key extends keyof Exclude<O, undefined>
? Rest extends []
? [...ValidKeys, Key] // case: nothing left in the array, and the last item correctly extended `keyof O`.
: Rest extends readonly PropertyKey[] // obligatory typeguard
? ValidatedKeys<Rest,Exclude<O, undefined>[Key], [...ValidKeys, Key]> // recurse
: never // impossible, we've sufficiently typechecked `Rest`
: [...ValidKeys, keyof Exclude<O, undefined>] // case: key doesn't exist on object at this level, adding `keyof O` will give a good type hint
: [...ValidKeys] // case: empty top level array. This gives a good typehint for a single incorrect string;
Getter:
// access a property recursively. Utilizes the fact that `T | never` === `T`
type GetNestedProp<K extends readonly PropertyKey[], O, MaybeUndef extends undefined = never> =
K extends readonly [infer Key, ...infer Rest]
? Key extends keyof O
? Rest extends []
? O[Key] | MaybeUndef // succesful exit, no more keys remaining in array. Union with undefined if needed
/* obligatory typeguard to validate the inferred `Rest` for recursion */
: Rest extends readonly PropertyKey[]
// If it's potentially undefined, We're going to recurse excluding the undefined, and then unify it with an undefined
? O[Key] extends infer Prop
? Prop extends undefined
? GetNestedProp<Rest, Exclude<Prop, undefined>, undefined>
: GetNestedProp<Rest,Prop, MaybeUndef>
: never // impossible, `infer Prop` has no constraint so will always succeed
:never // impossible, we've typechecked `Rest` sufficiently
: undefined // case: key doesn't exist on object at this level
: undefined; // case: empty top level array
为了让函数正确推断泛型,泛型需要作为可能的参数出现。我们想要的是 ValidKeys
,但如果没有 Keys
本身作为潜在参数,我们就无法做到这一点。所以我们使用 ...keys
参数的条件来强制它解析。
关于 return 类型,即使 GetNestedProp
可能与 undefined
联合,编译器也无法推断它肯定是在您的 else 分支的情况下被击中。因此,您可以使 return 类型成为这种笨拙的条件,或者 //@ts-expect-error
else 分支 return 语句具有更简单的 return 类型 GetNestedProp<Keys, Obj>
。该替代方案包含在 playground 中:
function getNestedItem<Obj, Keys extends readonly [keyof Obj, ...PropertyKey[]], ValidKeys extends ValidatedKeys<Keys, Obj>>(
obj: Obj,
...keys: ValidKeys extends Keys ? Keys : ValidKeys
): GetNestedProp<Keys, Obj> extends undefined ? GetNestedProp<Keys, Obj> | undefined : GetNestedProp<Keys,Obj> {
let level: any = obj;
for (const key of keys) {
if (level !== undefined) {
level = level[key];
} else {
return;
}
}
return level;
}
给定一个带有可选 属性 的类型,深入研究 属性 会将嵌套的 属性 类型转换为具有未定义的联合:
interface HasOpt {
a: { b: number };
aOpt?: {b: number };
}
declare const obj: HasOpt;
const ab = getNestedItem(obj, "a", "b") // number
const abOpt = getNestedItem(obj, "aOpt", "b") // number | undefined
我正在尝试制作一个辅助函数,它接受像 JSON 这样的嵌套对象,并允许在任意深度深度复制嵌套值。我理解可变元组类型并且可以让它们工作以传递元组 - 但我不知道如何 'map' 它们到任意深度的嵌套 Picks (它甚至可能是不可能的)。这是我想出的最好的 - 但仍然限于需要为 GetNestedValue 创建尽可能多的重载,因为我愿意支持。我理解各种错误,我只是想不出任何方法来满足编译器并在 return 值上完成类型。
// K is arbitrary length how to express N accessors deep? in TS without a loop?
type GetNestedValue<K extends string[], O extends any> = O[K[0]][K[1]][K[2]];
function getNestedItem<Keys extends string[], Obj>(
obj: Obj, ...keys: readonly [...Keys]
): GetNestedValue<Keys, Obj> extends undefined ? undefined : GetNestedValue<Keys, Obj> {
let level: any = obj;
for (const key of keys) {
if (level !== undefined) {
level = level[key];
} else {
return;
}
}
// this will return deepClone(level);
return level;
}
const obj = {one: 1, two: {three: {four: 4}}};
// I'd prefer 'known' shapes of obj here block form entering invalid keys.
const a = getNestedItem(obj, 'one', 'two');
// here - when arbitrarily trying to grab stuff from unknown inputs - I don't want
// a warning, rather the user would just need to check `if (b !== undefined)`
const b = getNestedItem(obj as any, 'one', 'two');
link 至 playground
有趣的是,您偏爱“已知”类型而拒绝使用未知类型,到处都是一堆 anys。我对可变元组类型一无所知,但这里有 3 个示例
export const getDeepProp = <Obj>(obj: Obj, ...keys: readonly [keyof Obj]) => {
const send: Record<string, unknown> = {};
keys.forEach(i=> {
type current = typeof obj[typeof i];
send[i] = obj[i] as current;
});
return send;
};
export function deepClone <O> (values:O): O {
if (values == null || typeof values != "object") return values;
const copy: Record<string, unknown> = {};
for (const attr in values) {
if (values[attr]) {
copy[attr] = deepClone(values[attr]);
}
}
return copy as O;
}
export function name<O> (params:O, ...keys: string[]) {
const a: Record<string,unknown> = {};
keys.forEach(key => {
a[key as keyof O] = typeof params[key as keyof O]==="object" ?
name(params[key as keyof O], ...Object.keys(params[key as keyof O])) : params[key as keyof O];
});
return a as O;
}
我首先要说的是:虽然这是一个有趣的思想实验,但由于它需要大量的递归,我不推荐这样做。
它需要两种递归类型,一种类型用于获取从对象类型推断出的一组有效键,另一种类型用于访问 属性 给定那些经过验证的键。对于 TypeScript < 4.5,深度限制将是一个长度为 10 的元组。
验证:
// walk through the keys and validate as we recurse. If we reach an invalid
// key, we return the currently validated set along with a type hint
type ValidatedKeys<K extends readonly PropertyKey[], O, ValidKeys extends readonly PropertyKey[] = []> =
K extends readonly [infer Key, ...infer Rest]
// Excluding undefined to allow `a?.b?.c`
? Key extends keyof Exclude<O, undefined>
? Rest extends []
? [...ValidKeys, Key] // case: nothing left in the array, and the last item correctly extended `keyof O`.
: Rest extends readonly PropertyKey[] // obligatory typeguard
? ValidatedKeys<Rest,Exclude<O, undefined>[Key], [...ValidKeys, Key]> // recurse
: never // impossible, we've sufficiently typechecked `Rest`
: [...ValidKeys, keyof Exclude<O, undefined>] // case: key doesn't exist on object at this level, adding `keyof O` will give a good type hint
: [...ValidKeys] // case: empty top level array. This gives a good typehint for a single incorrect string;
Getter:
// access a property recursively. Utilizes the fact that `T | never` === `T`
type GetNestedProp<K extends readonly PropertyKey[], O, MaybeUndef extends undefined = never> =
K extends readonly [infer Key, ...infer Rest]
? Key extends keyof O
? Rest extends []
? O[Key] | MaybeUndef // succesful exit, no more keys remaining in array. Union with undefined if needed
/* obligatory typeguard to validate the inferred `Rest` for recursion */
: Rest extends readonly PropertyKey[]
// If it's potentially undefined, We're going to recurse excluding the undefined, and then unify it with an undefined
? O[Key] extends infer Prop
? Prop extends undefined
? GetNestedProp<Rest, Exclude<Prop, undefined>, undefined>
: GetNestedProp<Rest,Prop, MaybeUndef>
: never // impossible, `infer Prop` has no constraint so will always succeed
:never // impossible, we've typechecked `Rest` sufficiently
: undefined // case: key doesn't exist on object at this level
: undefined; // case: empty top level array
为了让函数正确推断泛型,泛型需要作为可能的参数出现。我们想要的是 ValidKeys
,但如果没有 Keys
本身作为潜在参数,我们就无法做到这一点。所以我们使用 ...keys
参数的条件来强制它解析。
关于 return 类型,即使 GetNestedProp
可能与 undefined
联合,编译器也无法推断它肯定是在您的 else 分支的情况下被击中。因此,您可以使 return 类型成为这种笨拙的条件,或者 //@ts-expect-error
else 分支 return 语句具有更简单的 return 类型 GetNestedProp<Keys, Obj>
。该替代方案包含在 playground 中:
function getNestedItem<Obj, Keys extends readonly [keyof Obj, ...PropertyKey[]], ValidKeys extends ValidatedKeys<Keys, Obj>>(
obj: Obj,
...keys: ValidKeys extends Keys ? Keys : ValidKeys
): GetNestedProp<Keys, Obj> extends undefined ? GetNestedProp<Keys, Obj> | undefined : GetNestedProp<Keys,Obj> {
let level: any = obj;
for (const key of keys) {
if (level !== undefined) {
level = level[key];
} else {
return;
}
}
return level;
}
给定一个带有可选 属性 的类型,深入研究 属性 会将嵌套的 属性 类型转换为具有未定义的联合:
interface HasOpt {
a: { b: number };
aOpt?: {b: number };
}
declare const obj: HasOpt;
const ab = getNestedItem(obj, "a", "b") // number
const abOpt = getNestedItem(obj, "aOpt", "b") // number | undefined