TypeScript 通过点表示法选择多个属性。

huangapple go评论66阅读模式
英文:

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)=&gt;{
    let newObj = {}

    paths.forEach((path)=&gt;{
        const value = path.split(&#39;.&#39;).reduce((prev,curr)=&gt;{
            return prev ? prev[curr] : null;
        }
        , obj || self);
        function buildObj(key, value) {
            var object
            var result = object = {};
            var arr = key.split(&#39;.&#39;);
            for (var i = 0; i &lt; 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: &quot;mocha.banjo@somecompany.com&quot;,
    suspended: false,
    id: &#39;aiojefoij23498sdofnsfsdfoij&#39;,
    customSchemas: {
        Roster: {
            prop1: &#39;val1&#39;,
            prop2: &#39;val2&#39;,
            prop3: &#39;val3&#39;
        }
    },
    some: {
        deeply: {
            nested: {
                value: 2345945
            }
        }
    },
    names: {
        givenName: &#39;Mocha&#39;,
        familyName: &#39;Banjo&#39;,
        fullName: &#39;Mocha Banjo&#39;
    },
    phones: [{
        type: &#39;primary&#39;,
        value: &#39;+1 (000) 000-0000&#39;
    }]

}

const result = pickObjProps(obj, [&#39;primaryEmail&#39;, &#39;customSchemas.Roster&#39;, &#39;some.deeply.nested&#39;,])

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&lt;TObject, TPath extends string&gt; = 
    TPath extends `${infer TKey extends keyof TObject &amp; string}.${infer TRest}` ?
        PickByDotNotation&lt;TObject[TKey], TRest&gt; :
    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&lt;TObject, TPaths extends string[]&gt; = TPaths extends [
    infer TKey extends string,
    infer TRest extends string[],
]
    ? PickManyByDotNotation&lt;PickByDotNotation&lt;TObject, TKey&gt;, TRest&gt;
    : TPaths extends string
    ? PickByDotNotation&lt;TObject, TPaths&gt;
    : never

type PickByDotNotation&lt;TObject, TPath extends string&gt; =
    // Constraining TKey so we don&#39;t need to check if its keyof TObject
    TPath extends `${infer TKey extends keyof TObject &amp; string}.${infer TRest}`
    ? PickByDotNotation&lt;TObject[TKey], TRest&gt;
    : 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&lt;Test, [&#39;customer.email&#39;, &#39;custom.name&#39;]&gt;

// 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&lt;T, K&gt; 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&lt;T extends object, K extends string&gt;(
  obj: T, paths: K[]
): PickByDotNotation&lt;T, K&gt;

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 write pickObjProps({a: 1, b: 2}, [&quot;b&quot;, &quot;c&quot;]) 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 like phones.0.type or phones[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 because PickByDotNotation&lt;T, K&gt; 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&lt;T, K extends string&gt; = {
    [P in keyof T as P extends (
        K extends `${infer K0}.${string}` ? K0 : K
    ) ? P : never]:
    P extends K ? T[P] : PickByDotNotation&lt;
        T[P], K extends `${Exclude&lt;P, symbol&gt;}.${infer R}` ? R : never
    &gt;
} &amp; {} 

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 &quot;foo&quot; | &quot;bar.baz&quot; | &quot;bar.qux&quot; | &quot;blah.yuck&quot; then K extends `${infer K0}.${string}` ? K0 : K will be &quot;foo&quot; | &quot;bar&quot; | &quot;blah&quot;. 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&lt;T[P], K extends `${Exclude&lt;P, symbol&gt;}.${infer R}` ? R : never&gt;. 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&lt;P, symbol&gt;}.${infer R}` ? R : never&gt;. (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 &quot;foo&quot; | &quot;bar.baz&quot; | &quot;bar.qux&quot; | &quot;blah.yuck&quot; and P is &quot;bar&quot; then K extends `${Exclude&lt;P, symbol&gt;}.${infer R}` ? R : never&gt; will be &quot;baz&quot; | &quot;qux&quot;.

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&lt;{⋯}, &quot;⋯&quot; | &quot;⋯&quot; | &quot;⋯&quot; | ⋯&gt; which isn't what people want to see. By writing &amp; {} 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: &quot;mocha.banjo@somecompany.com&quot;,
    suspended: false,
    id: &#39;aiojefoij23498sdofnsfsdfoij&#39;,
    customSchemas: {
        Roster: {
            prop1: &#39;val1&#39;,
            prop2: &#39;val2&#39;,
            prop3: &#39;val3&#39;
        }
    },
    some: {
        deeply: {
            nested: {
                value: 2345945
            }
        }
    },
    names: {
        givenName: &#39;Mocha&#39;,
        familyName: &#39;Banjo&#39;,
        fullName: &#39;Mocha Banjo&#39;
    },
    phones: [{
        type: &#39;primary&#39;,
        value: &#39;+1 (000) 000-0000&#39;
    }]

}

const result = pickObjProps(obj, 
  [&#39;primaryEmail&#39;, &#39;customSchemas.Roster&#39;, &#39;some.deeply.nested&#39;]
);

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.

Playground link to code

答案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 &#39;type-fest&#39;
type PickDeepBaseOptions&lt;T = unknown&gt; = {
$required?: readonly (keyof T)[]
$optional?: readonly (keyof T)[]
}
type PickDeepOptions&lt;T&gt; = T extends unknown[] ? PickDeepOptions&lt;T[number]&gt; :
PickDeepBaseOptions&lt;T&gt; &amp; { [K in keyof T]?: PickDeepOptions&lt;T[K]&gt; }
type SetOptionalLax&lt;BaseType, Keys&gt; = SetOptional&lt;BaseType, Keys &amp; keyof BaseType&gt;
type RequiredKeys&lt;O&gt; = O extends { $required: readonly string[] } ? O[&#39;$required&#39;][number] : never
type OptionalKeys&lt;O&gt; = O extends { $optional: readonly string[] } ? O[&#39;$optional&#39;][number] : never
type KeysToTransform&lt;O&gt; = Exclude&lt;keyof O, keyof PickDeepBaseOptions&gt;
type KeysToCopy&lt;O&gt; = Exclude&lt;RequiredKeys&lt;O&gt; | OptionalKeys&lt;O&gt;, KeysToTransform&lt;O&gt;&gt;
type PickDeep&lt;T, O&gt; =
T extends unknown[] ? PickDeep&lt;T[number], O&gt;[] :
SetOptionalLax&lt;
{
[K in KeysToCopy&lt;O&gt; &amp; keyof T]: NonNullable&lt;T[K]&gt;
} &amp; {
[K in KeysToTransform&lt;O&gt; &amp; keyof T]: PickDeep&lt;NonNullable&lt;T[K]&gt;, O[K]&gt;
},
OptionalKeys&lt;O&gt;
&gt;
/* ===== Usage: ===== */
const obj = {
primaryEmail: &#39;mocha.banjo@somecompany.com&#39;,
suspended: false,
id: &#39;aiojefoij23498sdofnsfsdfoij&#39;,
customSchemas: {
Roster: {
prop1: &#39;val1&#39;,
prop2: &#39;val2&#39;,
prop3: &#39;val3&#39;
}
},
some: {
deeply: {
nested: {
value: 2345945
}
}
},
names: {
givenName: &#39;Mocha&#39;,
familyName: &#39;Banjo&#39;,
fullName: &#39;Mocha Banjo&#39;
},
phones: [{
type: &#39;primary&#39;,
value: &#39;+1 (000) 000-0000&#39;
}]
}
const pickOPtions = { 
$required: [&#39;primaryEmail&#39;],
$optional: [&#39;phones&#39;],
customSchemas: { $required: [&#39;Roster&#39;] },
some: { deeply: { $required: [&#39;nested&#39;] }},
phones: { $optional: [&#39;value&#39;] },
} satisfies PickDeepOptions&lt;typeof obj&gt;
type Target = PickDeep&lt;typeof obj, typeof pickOPtions&gt;
/* ===&gt;
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;
//  };
//}

Playground

英文:

Code from this answer.

It even works with arrays and optional properties.

type DeepPick&lt;T, K extends string&gt; = T extends object ? {
  [P in Head&lt;K&gt; &amp; keyof T]: T[P] extends readonly unknown[] ? DeepPick&lt;T[P][number], Tail&lt;Extract&lt;K, `${P}.${string}`&gt;&gt;&gt;[] : DeepPick&lt;T[P], Tail&lt;Extract&lt;K, `${P}.${string}`&gt;&gt;&gt;
} : T
type Head&lt;T extends string&gt; = T extends `${infer First}.${string}` ? First : T;
type Tail&lt;T extends string&gt; = 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&lt;TestUser, &quot;id&quot; | &quot;books.name&quot; | &quot;book.id&quot;&gt;;
//T = {
//  id: string;
//  books: {
//    name: string;
//  }[];
//  book: {
//    id: string;
//  };
//}

Playground

huangapple
  • 本文由 发表于 2023年5月14日 04:42:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/76244793.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定