From 669ade8aa52dd6071a5c97e842f21e58f3bd4415 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Thu, 19 Dec 2024 13:56:31 +0530 Subject: [PATCH 1/5] fix: distributed-pick with any --- source/distributed-pick.d.ts | 4 +--- test-d/distributed-pick.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/source/distributed-pick.d.ts b/source/distributed-pick.d.ts index 3b10058f6..3911f9398 100644 --- a/source/distributed-pick.d.ts +++ b/source/distributed-pick.d.ts @@ -80,6 +80,4 @@ if (pickedUnion.discriminant === 'A') { @category Object */ export type DistributedPick> = - ObjectType extends unknown - ? Pick> - : never; + {[Key in keyof ObjectType as Extract]: ObjectType[Key]}; diff --git a/test-d/distributed-pick.ts b/test-d/distributed-pick.ts index 93f7bcf2b..a4339fabb 100644 --- a/test-d/distributed-pick.ts +++ b/test-d/distributed-pick.ts @@ -76,3 +76,18 @@ if (pickedUnion.discriminant === 'A') { // @ts-expect-error const _bar = pickedUnion.bar; // eslint-disable-line @typescript-eslint/no-unsafe-assignment } + +// Preserves property modifiers +declare const test1: DistributedPick<{readonly 'a': 1; 'b'?: 2; readonly 'c'?: 3}, 'a' | 'b' | 'c'>; +expectType<{readonly 'a': 1; 'b'?: 2; readonly 'c'?: 3}>(test1); + +declare const test2: DistributedPick<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}, 'a' | 'b' | 'c'>; +expectType<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}>(test2); + +// Picks all keys when second type argument is any +declare const test3: DistributedPick<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}, any>; +expectType<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}>(test3); + +// Works with index signatures +declare const test4: DistributedPick<{[k: string]: unknown; a?: 1; b: '2'}, 'a' | 'b'>; +expectType<{a?: 1; b: '2'}>(test4); From 9d7471f62658691c2521f99df4560842a3e31d7c Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Thu, 19 Dec 2024 14:03:57 +0530 Subject: [PATCH 2/5] fix: set-* types with index signatures --- source/set-optional.d.ts | 4 +++- source/set-readonly.d.ts | 4 +++- source/set-required.d.ts | 4 +++- test-d/distributed-pick.ts | 6 +++--- test-d/set-optional.ts | 4 ++++ test-d/set-readonly.ts | 4 ++++ test-d/set-required.ts | 4 ++++ 7 files changed, 24 insertions(+), 6 deletions(-) diff --git a/source/set-optional.d.ts b/source/set-optional.d.ts index f2cfa9107..7c30623cb 100644 --- a/source/set-optional.d.ts +++ b/source/set-optional.d.ts @@ -1,4 +1,6 @@ +import type {DistributedPick} from './distributed-pick'; import type {Except} from './except'; +import type {KeysOfUnion} from './keys-of-union'; import type {Simplify} from './simplify'; /** @@ -32,6 +34,6 @@ export type SetOptional = // Pick just the keys that are readonly from the base type. Except & // Pick the keys that should be mutable from the base type and make them mutable. - Partial>> + Partial>> > : never; diff --git a/source/set-readonly.d.ts b/source/set-readonly.d.ts index bb6b19084..39d008e48 100644 --- a/source/set-readonly.d.ts +++ b/source/set-readonly.d.ts @@ -1,4 +1,6 @@ +import type {DistributedPick} from './distributed-pick'; import type {Except} from './except'; +import type {KeysOfUnion} from './keys-of-union'; import type {Simplify} from './simplify'; /** @@ -33,6 +35,6 @@ export type SetReadonly = BaseType extends unknown ? Simplify< Except & - Readonly>> + Readonly>> > : never; diff --git a/source/set-required.d.ts b/source/set-required.d.ts index d7b00f4bb..b4f0957fe 100644 --- a/source/set-required.d.ts +++ b/source/set-required.d.ts @@ -1,4 +1,6 @@ +import type {DistributedPick} from './distributed-pick'; import type {Except} from './except'; +import type {KeysOfUnion} from './keys-of-union'; import type {Simplify} from './simplify'; /** @@ -35,6 +37,6 @@ export type SetRequired = // Pick just the keys that are optional from the base type. Except & // Pick the keys that should be required from the base type and make them required. - Required>> + Required>> > : never; diff --git a/test-d/distributed-pick.ts b/test-d/distributed-pick.ts index a4339fabb..3391e5bab 100644 --- a/test-d/distributed-pick.ts +++ b/test-d/distributed-pick.ts @@ -84,10 +84,10 @@ expectType<{readonly 'a': 1; 'b'?: 2; readonly 'c'?: 3}>(test1); declare const test2: DistributedPick<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}, 'a' | 'b' | 'c'>; expectType<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}>(test2); -// Picks all keys when second type argument is any +// Picks all keys, if `KeyType` is `any` declare const test3: DistributedPick<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}, any>; expectType<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}>(test3); // Works with index signatures -declare const test4: DistributedPick<{[k: string]: unknown; a?: 1; b: '2'}, 'a' | 'b'>; -expectType<{a?: 1; b: '2'}>(test4); +declare const test4: DistributedPick<{[k: string]: unknown; a?: number; b: string}, 'a' | 'b'>; +expectType<{a?: number; b: string}>(test4); diff --git a/test-d/set-optional.ts b/test-d/set-optional.ts index aa7f8d1ac..8e869f55f 100644 --- a/test-d/set-optional.ts +++ b/test-d/set-optional.ts @@ -32,3 +32,7 @@ expectType<{readonly a?: number; b?: string; c?: boolean}>(variation7); // Does nothing, if `Keys` is `never`. declare const variation8: SetOptional<{a?: number; readonly b?: string; readonly c: boolean}, never>; expectType<{a?: number; readonly b?: string; readonly c: boolean}>(variation8); + +// Works with index signatures +declare const variation9: SetOptional<{[k: string]: unknown; a: number; b?: string}, 'a' | 'b'>; +expectType<{[k: string]: unknown; a?: number; b?: string}>(variation9); diff --git a/test-d/set-readonly.ts b/test-d/set-readonly.ts index 0ba0b9884..a8e39caa7 100644 --- a/test-d/set-readonly.ts +++ b/test-d/set-readonly.ts @@ -32,3 +32,7 @@ expectType<{readonly a?: number; readonly b: string; readonly c: boolean}>(varia // Does nothing, if `Keys` is `never`. declare const variation8: SetReadonly<{a: number; readonly b: string; readonly c: boolean}, never>; expectType<{a: number; readonly b: string; readonly c: boolean}>(variation8); + +// Works with index signatures +declare const variation9: SetReadonly<{[k: string]: unknown; a: number; readonly b: string}, 'a' | 'b'>; +expectType<{[k: string]: unknown; readonly a: number; readonly b: string}>(variation9); diff --git a/test-d/set-required.ts b/test-d/set-required.ts index 6e51f0333..80ee927dd 100644 --- a/test-d/set-required.ts +++ b/test-d/set-required.ts @@ -36,3 +36,7 @@ expectType<{readonly a: number; b: string; c: boolean}>(variation8); // Does nothing, if `Keys` is `never`. declare const variation9: SetRequired<{a?: number; readonly b?: string; readonly c: boolean}, never>; expectType<{a?: number; readonly b?: string; readonly c: boolean}>(variation9); + +// Works with index signatures +declare const variation10: SetRequired<{[k: string]: unknown; a?: number; b: string}, 'a' | 'b'>; +expectType<{[k: string]: unknown; a: number; b: string}>(variation10); From 19d58e391bdc8b5e895c92f03090243571e5c096 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Thu, 19 Dec 2024 22:36:14 +0530 Subject: [PATCH 3/5] fix: add new internal homomorphic-pick type and revert distributed-pick changes --- source/distributed-pick.d.ts | 4 ++- source/internal/object.d.ts | 42 +++++++++++++++++++++++ source/set-optional.d.ts | 4 +-- source/set-readonly.d.ts | 4 +-- source/set-required.d.ts | 4 +-- test-d/distributed-pick.ts | 4 --- test-d/internal/homomorphic-pick.ts | 53 +++++++++++++++++++++++++++++ 7 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 test-d/internal/homomorphic-pick.ts diff --git a/source/distributed-pick.d.ts b/source/distributed-pick.d.ts index 3911f9398..3b10058f6 100644 --- a/source/distributed-pick.d.ts +++ b/source/distributed-pick.d.ts @@ -80,4 +80,6 @@ if (pickedUnion.discriminant === 'A') { @category Object */ export type DistributedPick> = - {[Key in keyof ObjectType as Extract]: ObjectType[Key]}; + ObjectType extends unknown + ? Pick> + : never; diff --git a/source/internal/object.d.ts b/source/internal/object.d.ts index 5a6edda22..841d0d726 100644 --- a/source/internal/object.d.ts +++ b/source/internal/object.d.ts @@ -1,5 +1,6 @@ import type {Simplify} from '../simplify'; import type {UnknownArray} from '../unknown-array'; +import type {KeysOfUnion} from '../keys-of-union'; import type {FilterDefinedKeys, FilterOptionalKeys} from './keys'; import type {NonRecursiveType} from './type'; import type {ToString} from './string'; @@ -80,3 +81,44 @@ export type UndefinedToOptional = Simplify< [Key in keyof Pick>]?: Exclude; } >; + +/** +Works similar to the built-in `Pick` utility type, except for the following differences: +- Distributes over union types and allows picking keys from any member of the union type. +- Primitives types are returned as-is. +- Picks all keys if `Keys` is `any`. +- Doesn't pick `number` from a `string` index signature. + +@example +``` +type ImageUpload = { + url: string; + size: number; + thumbnailUrl: string; +}; + +type VideoUpload = { + url: string; + duration: number; + encodingFormat: string; +}; + +// Distributes over union types and allows picking keys from any member of the union type +type MediaDisplay = HomomorphicPick; +//=> {url: string; size: number} | {url: string; duration: number} + +// Primitive types are returned as-is +type Primitive = HomomorphicPick; +//=> string | number + +// Picks all keys if `Keys` is `any` +type Any = HomomorphicPick<{a: 1; b: 2} | {c: 3}, any>; +//=> {a: 1; b: 2} | {c: 3} + +// Doesn't pick `number` from a `string` index signature +type IndexSignature = HomomorphicPick<{[k: string]: unknown}, number>; +//=> {} +*/ +export type HomomorphicPick> = { + [P in keyof T as Extract]: T[P] +}; diff --git a/source/set-optional.d.ts b/source/set-optional.d.ts index 7c30623cb..b13a542cd 100644 --- a/source/set-optional.d.ts +++ b/source/set-optional.d.ts @@ -1,5 +1,5 @@ -import type {DistributedPick} from './distributed-pick'; import type {Except} from './except'; +import type {HomomorphicPick} from './internal'; import type {KeysOfUnion} from './keys-of-union'; import type {Simplify} from './simplify'; @@ -34,6 +34,6 @@ export type SetOptional = // Pick just the keys that are readonly from the base type. Except & // Pick the keys that should be mutable from the base type and make them mutable. - Partial>> + Partial>> > : never; diff --git a/source/set-readonly.d.ts b/source/set-readonly.d.ts index 39d008e48..aedf5e05d 100644 --- a/source/set-readonly.d.ts +++ b/source/set-readonly.d.ts @@ -1,5 +1,5 @@ -import type {DistributedPick} from './distributed-pick'; import type {Except} from './except'; +import type {HomomorphicPick} from './internal'; import type {KeysOfUnion} from './keys-of-union'; import type {Simplify} from './simplify'; @@ -35,6 +35,6 @@ export type SetReadonly = BaseType extends unknown ? Simplify< Except & - Readonly>> + Readonly>> > : never; diff --git a/source/set-required.d.ts b/source/set-required.d.ts index b4f0957fe..daab11f27 100644 --- a/source/set-required.d.ts +++ b/source/set-required.d.ts @@ -1,5 +1,5 @@ -import type {DistributedPick} from './distributed-pick'; import type {Except} from './except'; +import type {HomomorphicPick} from './internal'; import type {KeysOfUnion} from './keys-of-union'; import type {Simplify} from './simplify'; @@ -37,6 +37,6 @@ export type SetRequired = // Pick just the keys that are optional from the base type. Except & // Pick the keys that should be required from the base type and make them required. - Required>> + Required>> > : never; diff --git a/test-d/distributed-pick.ts b/test-d/distributed-pick.ts index 3391e5bab..2fcde3c51 100644 --- a/test-d/distributed-pick.ts +++ b/test-d/distributed-pick.ts @@ -84,10 +84,6 @@ expectType<{readonly 'a': 1; 'b'?: 2; readonly 'c'?: 3}>(test1); declare const test2: DistributedPick<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}, 'a' | 'b' | 'c'>; expectType<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}>(test2); -// Picks all keys, if `KeyType` is `any` -declare const test3: DistributedPick<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}, any>; -expectType<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}>(test3); - // Works with index signatures declare const test4: DistributedPick<{[k: string]: unknown; a?: number; b: string}, 'a' | 'b'>; expectType<{a?: number; b: string}>(test4); diff --git a/test-d/internal/homomorphic-pick.ts b/test-d/internal/homomorphic-pick.ts new file mode 100644 index 000000000..c32d46e87 --- /dev/null +++ b/test-d/internal/homomorphic-pick.ts @@ -0,0 +1,53 @@ +import {expectType} from 'tsd'; +import type {HomomorphicPick} from '../../source/internal'; + +// Picks specified keys +declare const test1: HomomorphicPick<{a: 1; b: 2; c: 3}, 'a' | 'b'>; +expectType<{a: 1; b: 2}>(test1); + +// Works with unions +declare const test2: HomomorphicPick<{a: 1; b: 2} | {a: 3; c: 4}, 'a'>; +expectType<{a: 1} | {a: 3}>(test2); + +declare const test3: HomomorphicPick<{a: 1; b: 2} | {c: 3; d: 4}, 'a' | 'c'>; +expectType<{a: 1} | {c: 3}>(test3); + +// Preserves property modifiers +declare const test4: HomomorphicPick<{readonly a: 1; b?: 2; readonly c?: 3}, 'a' | 'c'>; +expectType<{readonly a: 1; readonly c?: 3}>(test4); + +declare const test5: HomomorphicPick<{readonly a: 1; b?: 2} | {readonly c?: 3; d?: 4}, 'a' | 'c'>; +expectType<{readonly a: 1} | {readonly c?: 3}>(test5); + +// Passes through primitives unchanged +declare const test6: HomomorphicPick; +expectType(test6); + +declare const test7: HomomorphicPick; +expectType(test7); + +declare const test8: HomomorphicPick; +expectType(test8); + +declare const test9: HomomorphicPick; +expectType(test9); + +declare const test10: HomomorphicPick; +expectType(test10); + +// Picks all keys, if `KeyType` is `any` +declare const test11: HomomorphicPick<{readonly a: 1; b?: 2} | {readonly c?: 3}, any>; +expectType<{readonly a: 1; b?: 2} | {readonly c?: 3}>(test11); + +// Picks no keys, if `KeyType` is `never` +declare const test12: HomomorphicPick<{a: 1; b: 2}, never>; +expectType<{}>(test12); + +// Works with index signatures +declare const test13: HomomorphicPick<{[k: string]: unknown; a: 1; b: 2}, 'a' | 'b'>; +expectType<{a: 1; b: 2}>(test13); + +// Doesn't pick `number` from a `string` index signature +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style +declare const test14: HomomorphicPick<{[k: string]: unknown}, number>; +expectType<{}>(test14); From 07ca4b1e7bc67d4b15dd265787ea902b24370d28 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 25 Dec 2024 17:53:21 +0100 Subject: [PATCH 4/5] Update object.d.ts --- source/internal/object.d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/internal/object.d.ts b/source/internal/object.d.ts index 841d0d726..485d88a70 100644 --- a/source/internal/object.d.ts +++ b/source/internal/object.d.ts @@ -92,15 +92,15 @@ Works similar to the built-in `Pick` utility type, except for the following diff @example ``` type ImageUpload = { - url: string; - size: number; - thumbnailUrl: string; + url: string; + size: number; + thumbnailUrl: string; }; type VideoUpload = { - url: string; - duration: number; - encodingFormat: string; + url: string; + duration: number; + encodingFormat: string; }; // Distributes over union types and allows picking keys from any member of the union type From f48f14a4f94430d02737be51e44092ad56c7a698 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 25 Dec 2024 17:53:54 +0100 Subject: [PATCH 5/5] Update object.d.ts --- source/internal/object.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/internal/object.d.ts b/source/internal/object.d.ts index 485d88a70..bbd63a411 100644 --- a/source/internal/object.d.ts +++ b/source/internal/object.d.ts @@ -93,8 +93,8 @@ Works similar to the built-in `Pick` utility type, except for the following diff ``` type ImageUpload = { url: string; - size: number; - thumbnailUrl: string; + size: number; + thumbnailUrl: string; }; type VideoUpload = {