英文:
TypeScript Pick MANY Properties By Dot Notation
问题
type PickManyByDotNotation<TObject, TPaths extends string[]> = TPaths extends [
infer TKey,
...infer TRest
] ? TKey extends string ? TRest extends string[] ?
PickByDotNotation<TObject, TKey> & PickManyByDotNotation<TObject[TKey], TRest> :
never : never : TPaths extends string ? PickByDotNotation<TObject, TPaths> : never;
type PickByDotNotation<TObject, TPath extends string> =
TPath extends `${infer TKey}.${infer TRest}` ?
TKey extends keyof TObject ?
PickByDotNotation<TObject[TKey], TRest> : never :
TPath extends keyof TObject ?
TObject[TPath] : never;
希望这可以帮到你。如果你有其他问题,请随时告诉我。
英文:
I have this sloppy, hacked together function in JavaScript that allows you to pick properties from an object using dot notation:
<!-- begin snippet: js hide: false console: true babel: true -->
<!-- language: lang-js -->
const pickObjProps = (obj,paths)=>{
let newObj = {}
paths.forEach((path)=>{
const value = path.split('.').reduce((prev,curr)=>{
return prev ? prev[curr] : null;
}
, obj || self);
function buildObj(key, value) {
var object
var result = object = {};
var arr = key.split('.');
for (var i = 0; i < arr.length - 1; i++) {
object = object[arr[i]] = {};
}
object[arr[arr.length - 1]] = value;
return result;
}
newObj = Object.assign(newObj, {
...buildObj(path, value)
})
}
)
return newObj
}
const obj = {
primaryEmail: "mocha.banjo@somecompany.com",
suspended: false,
id: 'aiojefoij23498sdofnsfsdfoij',
customSchemas: {
Roster: {
prop1: 'val1',
prop2: 'val2',
prop3: 'val3'
}
},
some: {
deeply: {
nested: {
value: 2345945
}
}
},
names: {
givenName: 'Mocha',
familyName: 'Banjo',
fullName: 'Mocha Banjo'
},
phones: [{
type: 'primary',
value: '+1 (000) 000-0000'
}]
}
const result = pickObjProps(obj, ['primaryEmail', 'customSchemas.Roster', 'some.deeply.nested',])
console.log(result)
<!-- end snippet -->
The function works as I intend it to. However, I want to type the function in TypeScript and am having a hell of a time.
I stumbled across another post which gave me some insight on how to possibly type it:
type PickByDotNotation<TObject, TPath extends string> =
TPath extends `${infer TKey extends keyof TObject & string}.${infer TRest}` ?
PickByDotNotation<TObject[TKey], TRest> :
TPath extends keyof TObject ?
TObject[TPath] :
never
In trying to type the function, I am trying to create a new type called PickManyByDotNotation
that takes two generic arguments:
- An Object
- A array of strings
This is as far as I have gotten:
type PickManyByDotNotation<TObject, TPaths extends string[]> = TPaths extends [
infer TKey extends string,
infer TRest extends string[],
]
? PickManyByDotNotation<PickByDotNotation<TObject, TKey>, TRest>
: TPaths extends string
? PickByDotNotation<TObject, TPaths>
: never
type PickByDotNotation<TObject, TPath extends string> =
// Constraining TKey so we don't need to check if its keyof TObject
TPath extends `${infer TKey extends keyof TObject & string}.${infer TRest}`
? PickByDotNotation<TObject[TKey], TRest>
: TPath extends keyof TObject
? TObject[TPath]
: never
The idea would be to use the type as such:
interface Test {
customer: {
email: string;
name: string;
phone: string;
id: string
};
};
type PickMany = PickManyByDotNotation<Test, ['customer.email', 'custom.name']>
// which would theoretically return something like:
//
// customer: {
// email: string
// name: string
// }
I am pulling my hair out at this point, and am actually very embarrassed to be posting.
If you could help me finish the type PickManyByDotNotation
and or possibly give me some insight on how to property type the function, I would be more than grateful.
答案1
得分: 2
以下是您要翻译的代码部分:
type PickByDotNotation<T, K extends string> = {
[P in keyof T as P extends (
K extends `${infer K0}.${string}` ? K0 : K
) ? P : never]:
P extends K ? T[P] : PickByDotNotation<
T[P], K extends `${Exclude<P, symbol>}.${infer R}` ? R : never
>
} & {}
declare function pickObjProps<T extends object, K extends string>(
obj: T, paths: K[]
): PickByDotNotation<T, K>
在上述代码中,您定义了一个 PickByDotNotation
类型,以及一个名为 pickObjProps
的函数,用于根据指定的路径从对象中选择属性。此代码通过模板文字类型和条件类型使用 TypeScript 的高级功能来实现此目标。
英文:
The goal here is to write a PickByDotNotation<T, K>
type where T
is some object type and K
is a string literal type corresponding to a dotted path in T
, or possibly a union of such dotted path string types. And the output would be a supertype of T
containing only the paths that start with those mentioned in K
.
Once such a type exists you can give pickObjProps()
a call signature like
declare function pickObjProps<T extends object, K extends string>(
obj: T, paths: K[]
): PickByDotNotation<T, K>
A few caveats/disclaimers to get out of the way in the beginning:
-
I'm not worried about the implementation of
pickObjProps()
or getting the compiler to type check that the implementation satisfies the call signature. I'm assuming the implementation is correct (or if not, it's out of scope here) and that you will use whatever means necessary to get it to compiler with the call signature (like using type assertions or a single-call-signature overload etc). -
I'm not worried about what happens if
K
has any invalid entries; if you writepickObjProps({a: 1, b: 2}, ["b", "c"])
it will be accepted and produce a value of type{b: number}
. -
I'm only really concerned with non-recursive object types for
T
, without index signatures and without optional properties, and without union-typed properties, and without trying to index into arrays with numeric properties likephones.0.type
orphones[0].type
. I tested my code against the example call in the question and that's it. There are lots of possible complications with types, and becausePickByDotNotation<T, K>
is necessarily deeply recursive, it's quite possible that my implementation will do something unexpected in the face of some of these. Such issues can often be worked around, but fixing them can sometimes require a complete refactoring. So beware!
So, my implementation looks like this:
type PickByDotNotation<T, K extends string> = {
[P in keyof T as P extends (
K extends `${infer K0}.${string}` ? K0 : K
) ? P : never]:
P extends K ? T[P] : PickByDotNotation<
T[P], K extends `${Exclude<P, symbol>}.${infer R}` ? R : never
>
} & {}
I'm using key remapping in mapped types to filter the keys of T
. A mapped type of the form {[P in keyof T as ⋯ extends ⋯ ? P : never]: ⋯}
will include all keys where the ⋯ extends ⋯
conditional type is true, and suppress all keys where it is false.
In the above, the check is whether the key P
extends K extends `${infer K0}.${string}` ? K0 : K
. This is a template literal type that parses K
to get the part before the first dot, if there is a dot. So if K
is "foo" | "bar.baz" | "bar.qux" | "blah.yuck"
then K extends `${infer K0}.${string}` ? K0 : K
will be "foo" | "bar" | "blah"
. So we're keeping all keys that appear as the first part of the paths in K
, and suppressing all keys that don't.
As for the value type of the property, that's P extends K ? T[P] : PickByDotNotation<T[P], K extends `${Exclude<P, symbol>}.${infer R}` ? R : never>
. If P
happens to be the same as K
then it means this isn't dotted and we just return keep the property type T[P]
like a normal Pick
. Otherwise K
is dotted and we want to grab the part after the dot and do a recursive call to PickByDotNotation
with T[P]
as the new object. The part after the dot is K extends `${Exclude<P, symbol>}.${infer R}` ? R : never>
. (I'm using the Exclude
utility type to ignore the possibility that P
is a symbol
type which can't be treated like a string.) So if K
is "foo" | "bar.baz" | "bar.qux" | "blah.yuck"
and P
is "bar"
then K extends `${Exclude<P, symbol>}.${infer R}` ? R : never>
will be "baz" | "qux"
.
That's mostly it. I added an intersection with the empty object type {}
, but that's just for cosmetic purposes. If you leave it out then the compiler will show the output type of pickObjProps()
will display in IntelliSense quickinfo as PickByDotNotation<{⋯}, "⋯" | "⋯" | "⋯" | ⋯>
which isn't what people want to see. By writing & {}
it prompts the type display to evaluate it fully and you get the keys and values written out.
Okay let's test it out on
const obj = {
primaryEmail: "mocha.banjo@somecompany.com",
suspended: false,
id: 'aiojefoij23498sdofnsfsdfoij',
customSchemas: {
Roster: {
prop1: 'val1',
prop2: 'val2',
prop3: 'val3'
}
},
some: {
deeply: {
nested: {
value: 2345945
}
}
},
names: {
givenName: 'Mocha',
familyName: 'Banjo',
fullName: 'Mocha Banjo'
},
phones: [{
type: 'primary',
value: '+1 (000) 000-0000'
}]
}
const result = pickObjProps(obj,
['primaryEmail', 'customSchemas.Roster', 'some.deeply.nested']
);
The type of result
can be seen as
/* const result: {
primaryEmail: string;
customSchemas: {
Roster: {
prop1: string;
prop2: string;
prop3: string;
};
};
some: {
deeply: {
nested: {
value: number;
};
};
};
} */
Looks good. Only the properties whose paths begin with the elements of that array are present in the output type.
答案2
得分: 0
这是代码部分,已经被排除,以下是你提供的文本的翻译部分:
这并不真正回答你的问题,但我之前尝试解决过相同的问题,一开始也选择了点表示法。但我需要更改某些属性的可为空性,即基本类型中的某些属性是可选的,但在选择的类型中应该是必需的,反之亦然,这在点表示法中很难表达。最终我使用了一种对象表示法,并认为它可能会有所帮助。使用对象表示法还提供了更好的自动完成/智能感知支持,避免了拼写错误,避免了在嵌套属性中重复外部属性名称。
这是我如何实现的。请注意,在我的用例中,目标类型的属性的可为空性不是从原始类型继承的,而是在对象中定义的,如果这不是你想要的,你可能需要稍微修改一下。
import { SetOptional } from 'type-fest'
type PickDeepBaseOptions<T = unknown> = {
$required?: readonly (keyof T)[]
$optional?: readonly (keyof T)[]
}
type PickDeepOptions<T> = T extends unknown[] ? PickDeepOptions<T[number]> :
PickDeepBaseOptions<T> & { [K in keyof T]?: PickDeepOptions<T[K]> }
type SetOptionalLax<BaseType, Keys> = SetOptional<BaseType, Keys & keyof BaseType>
type RequiredKeys<O> = O extends { $required: readonly string[] } ? O['$required'][number] : never
type OptionalKeys<O> = O extends { $optional: readonly string[] } ? O['$optional'][number] : never
type KeysToTransform<O> = Exclude<keyof O, keyof PickDeepBaseOptions>
type KeysToCopy<O> = Exclude<RequiredKeys<O> | OptionalKeys<O>, KeysToTransform<O>>
type PickDeep<T, O> =
T extends unknown[] ? PickDeep<T[number], O>[] :
SetOptionalLax<
{
[K in KeysToCopy<O> & keyof T]: NonNullable<T[K]>
} & {
[K in KeysToTransform<O> & keyof T]: PickDeep<NonNullable<T[K]>, O[K]>
},
OptionalKeys<O>
>
这是用法示例:
/* ===== Usage: ===== */
const obj = {
primaryEmail: 'mocha.banjo@somecompany.com',
suspended: false,
id: 'aiojefoij23498sdofnsfsdfoij',
customSchemas: {
Roster: {
prop1: 'val1',
prop2: 'val2',
prop3: 'val3'
}
},
some: {
deeply: {
nested: {
value: 2345945
}
}
},
names: {
givenName: 'Mocha',
familyName: 'Banjo',
fullName: 'Mocha Banjo'
},
phones: [{
type: 'primary',
value: '+1 (000) 000-0000'
}]
}
const pickOptions = {
$required: ['primaryEmail'],
$optional: ['phones'],
customSchemas: { $required: ['Roster'] },
some: { deeply: { $required: ['nested'] }},
phones: { $optional: ['value'] },
} satisfies PickDeepOptions<typeof obj>
type Target = PickDeep<typeof obj, typeof pickOptions>
我还将phones属性添加到了pickOptions对象中,以演示**$optional**和在数组中选择的功能。我理解你可能已经有一个使用点表示法的选择函数。在这种情况下,你可以创建一个辅助函数来将pickOptions对象转换为点表示法键的列表,或者创建一个新的选择函数。
英文:
This does not really answer your question, but I had tried to solve the same problem before, and also chose dot notation at first. But I needed to change the nullability of some properties, i.e some properties were optional in the base type but should be required in the picked type and vice versa, which is cumbersome to express in dot notation. I ended up using a kind of object notation and think it may be of help. Using object notation also offers better auto-complete / intellisense support, avoid typos, avoid having to repeat the outer property name for nested properties.
This was how I implemented it. Please note that in my use case the nullability of properties of target type is not inherited from the original type but defined in the object instead, you may have to modify a little if it's not what you want.
import { SetOptional } from 'type-fest'
type PickDeepBaseOptions<T = unknown> = {
$required?: readonly (keyof T)[]
$optional?: readonly (keyof T)[]
}
type PickDeepOptions<T> = T extends unknown[] ? PickDeepOptions<T[number]> :
PickDeepBaseOptions<T> & { [K in keyof T]?: PickDeepOptions<T[K]> }
type SetOptionalLax<BaseType, Keys> = SetOptional<BaseType, Keys & keyof BaseType>
type RequiredKeys<O> = O extends { $required: readonly string[] } ? O['$required'][number] : never
type OptionalKeys<O> = O extends { $optional: readonly string[] } ? O['$optional'][number] : never
type KeysToTransform<O> = Exclude<keyof O, keyof PickDeepBaseOptions>
type KeysToCopy<O> = Exclude<RequiredKeys<O> | OptionalKeys<O>, KeysToTransform<O>>
type PickDeep<T, O> =
T extends unknown[] ? PickDeep<T[number], O>[] :
SetOptionalLax<
{
[K in KeysToCopy<O> & keyof T]: NonNullable<T[K]>
} & {
[K in KeysToTransform<O> & keyof T]: PickDeep<NonNullable<T[K]>, O[K]>
},
OptionalKeys<O>
>
/* ===== Usage: ===== */
const obj = {
primaryEmail: 'mocha.banjo@somecompany.com',
suspended: false,
id: 'aiojefoij23498sdofnsfsdfoij',
customSchemas: {
Roster: {
prop1: 'val1',
prop2: 'val2',
prop3: 'val3'
}
},
some: {
deeply: {
nested: {
value: 2345945
}
}
},
names: {
givenName: 'Mocha',
familyName: 'Banjo',
fullName: 'Mocha Banjo'
},
phones: [{
type: 'primary',
value: '+1 (000) 000-0000'
}]
}
const pickOPtions = {
$required: ['primaryEmail'],
$optional: ['phones'],
customSchemas: { $required: ['Roster'] },
some: { deeply: { $required: ['nested'] }},
phones: { $optional: ['value'] },
} satisfies PickDeepOptions<typeof obj>
type Target = PickDeep<typeof obj, typeof pickOPtions>
/* ===>
type Target = {
primaryEmail: string;
customSchemas: {
Roster: {
prop1: string;
prop2: string;
prop3: string;
};
};
some: {
deeply: {
nested: {
value: number;
};
};
};
phones?: {
value?: string | undefined;
}[] | undefined;
}
*/
I also add phones property to the pickOptions object to demonstrate $optional and picking inside arrays.
I understand that you may already have a pick function that uses dot notation. In that case you can create either a helper function to transform the pickOptions object to list of dot notation keys, or a new pick function
答案3
得分: 0
以下是翻译好的部分:
类型 DeepPick<T, K extends string> = T extends object ? {
[P in Head<K> & keyof T]: T[P] extends readonly unknown[] ? DeepPick<T[P][number], Tail<Extract<K, `${P}.${string}`>>>[] : DeepPick<T[P], Tail<Extract<K, `${P}.${string}`>>>
} : T
类型 Head<T extends string> = T extends `${infer First}.${string}` ? First : T;
类型 Tail<T extends string> = T extends `${string}.${infer Rest}` ? Rest : never;
使用示例:
接口 TestBook {
id: string;
name: string;
}
接口 TestUser {
id: string;
email: string;
books: TestBook[];
book: TestBook,
}
类型 T = DeepPick<TestUser, "id" | "books.name" | "book.id">;
//T = {
// id: string;
// books: {
// name: string;
// }[];
// book: {
// id: string;
// };
//}
英文:
Code from this answer.
It even works with arrays and optional properties.
type DeepPick<T, K extends string> = T extends object ? {
[P in Head<K> & keyof T]: T[P] extends readonly unknown[] ? DeepPick<T[P][number], Tail<Extract<K, `${P}.${string}`>>>[] : DeepPick<T[P], Tail<Extract<K, `${P}.${string}`>>>
} : T
type Head<T extends string> = T extends `${infer First}.${string}` ? First : T;
type Tail<T extends string> = T extends `${string}.${infer Rest}` ? Rest : never;
Usage example:
interface TestBook {
id: string;
name: string;
}
interface TestUser {
id: string;
email: string;
books: TestBook[];
book: TestBook,
}
type T = DeepPick<TestUser, "id" | "books.name" | "book.id">;
//T = {
// id: string;
// books: {
// name: string;
// }[];
// book: {
// id: string;
// };
//}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论