Skip to content

Commit cb269ff

Browse files
authored
SetOptional/SetRequired/SetReadonly: Fix instantiations with index signatures (#1014)
1 parent 59517cb commit cb269ff

9 files changed

Lines changed: 127 additions & 3 deletions

File tree

source/internal/object.d.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {Simplify} from '../simplify';
22
import type {UnknownArray} from '../unknown-array';
3+
import type {KeysOfUnion} from '../keys-of-union';
34
import type {FilterDefinedKeys, FilterOptionalKeys} from './keys';
45
import type {NonRecursiveType} from './type';
56
import type {ToString} from './string';
@@ -80,3 +81,44 @@ export type UndefinedToOptional<T extends object> = Simplify<
8081
[Key in keyof Pick<T, FilterOptionalKeys<T>>]?: Exclude<T[Key], undefined>;
8182
}
8283
>;
84+
85+
/**
86+
Works similar to the built-in `Pick` utility type, except for the following differences:
87+
- Distributes over union types and allows picking keys from any member of the union type.
88+
- Primitives types are returned as-is.
89+
- Picks all keys if `Keys` is `any`.
90+
- Doesn't pick `number` from a `string` index signature.
91+
92+
@example
93+
```
94+
type ImageUpload = {
95+
url: string;
96+
size: number;
97+
thumbnailUrl: string;
98+
};
99+
100+
type VideoUpload = {
101+
url: string;
102+
duration: number;
103+
encodingFormat: string;
104+
};
105+
106+
// Distributes over union types and allows picking keys from any member of the union type
107+
type MediaDisplay = HomomorphicPick<ImageUpload | VideoUpload, "url" | "size" | "duration">;
108+
//=> {url: string; size: number} | {url: string; duration: number}
109+
110+
// Primitive types are returned as-is
111+
type Primitive = HomomorphicPick<string | number, 'toUpperCase' | 'toString'>;
112+
//=> string | number
113+
114+
// Picks all keys if `Keys` is `any`
115+
type Any = HomomorphicPick<{a: 1; b: 2} | {c: 3}, any>;
116+
//=> {a: 1; b: 2} | {c: 3}
117+
118+
// Doesn't pick `number` from a `string` index signature
119+
type IndexSignature = HomomorphicPick<{[k: string]: unknown}, number>;
120+
//=> {}
121+
*/
122+
export type HomomorphicPick<T, Keys extends KeysOfUnion<T>> = {
123+
[P in keyof T as Extract<P, Keys>]: T[P]
124+
};

source/set-optional.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {Except} from './except';
2+
import type {HomomorphicPick} from './internal';
3+
import type {KeysOfUnion} from './keys-of-union';
24
import type {Simplify} from './simplify';
35

46
/**
@@ -32,6 +34,6 @@ export type SetOptional<BaseType, Keys extends keyof BaseType> =
3234
// Pick just the keys that are readonly from the base type.
3335
Except<BaseType, Keys> &
3436
// Pick the keys that should be mutable from the base type and make them mutable.
35-
Partial<Except<BaseType, Exclude<keyof BaseType, Keys>>>
37+
Partial<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
3638
>
3739
: never;

source/set-readonly.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {Except} from './except';
2+
import type {HomomorphicPick} from './internal';
3+
import type {KeysOfUnion} from './keys-of-union';
24
import type {Simplify} from './simplify';
35

46
/**
@@ -33,6 +35,6 @@ export type SetReadonly<BaseType, Keys extends keyof BaseType> =
3335
BaseType extends unknown
3436
? Simplify<
3537
Except<BaseType, Keys> &
36-
Readonly<Except<BaseType, Exclude<keyof BaseType, Keys>>>
38+
Readonly<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
3739
>
3840
: never;

source/set-required.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {Except} from './except';
2+
import type {HomomorphicPick} from './internal';
3+
import type {KeysOfUnion} from './keys-of-union';
24
import type {Simplify} from './simplify';
35

46
/**
@@ -35,6 +37,6 @@ export type SetRequired<BaseType, Keys extends keyof BaseType> =
3537
// Pick just the keys that are optional from the base type.
3638
Except<BaseType, Keys> &
3739
// Pick the keys that should be required from the base type and make them required.
38-
Required<Except<BaseType, Exclude<keyof BaseType, Keys>>>
40+
Required<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
3941
>
4042
: never;

test-d/distributed-pick.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,14 @@ if (pickedUnion.discriminant === 'A') {
7676
// @ts-expect-error
7777
const _bar = pickedUnion.bar; // eslint-disable-line @typescript-eslint/no-unsafe-assignment
7878
}
79+
80+
// Preserves property modifiers
81+
declare const test1: DistributedPick<{readonly 'a': 1; 'b'?: 2; readonly 'c'?: 3}, 'a' | 'b' | 'c'>;
82+
expectType<{readonly 'a': 1; 'b'?: 2; readonly 'c'?: 3}>(test1);
83+
84+
declare const test2: DistributedPick<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}, 'a' | 'b' | 'c'>;
85+
expectType<{readonly 'a': 1; 'b'?: 2} | {readonly 'c'?: 3}>(test2);
86+
87+
// Works with index signatures
88+
declare const test4: DistributedPick<{[k: string]: unknown; a?: number; b: string}, 'a' | 'b'>;
89+
expectType<{a?: number; b: string}>(test4);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {expectType} from 'tsd';
2+
import type {HomomorphicPick} from '../../source/internal';
3+
4+
// Picks specified keys
5+
declare const test1: HomomorphicPick<{a: 1; b: 2; c: 3}, 'a' | 'b'>;
6+
expectType<{a: 1; b: 2}>(test1);
7+
8+
// Works with unions
9+
declare const test2: HomomorphicPick<{a: 1; b: 2} | {a: 3; c: 4}, 'a'>;
10+
expectType<{a: 1} | {a: 3}>(test2);
11+
12+
declare const test3: HomomorphicPick<{a: 1; b: 2} | {c: 3; d: 4}, 'a' | 'c'>;
13+
expectType<{a: 1} | {c: 3}>(test3);
14+
15+
// Preserves property modifiers
16+
declare const test4: HomomorphicPick<{readonly a: 1; b?: 2; readonly c?: 3}, 'a' | 'c'>;
17+
expectType<{readonly a: 1; readonly c?: 3}>(test4);
18+
19+
declare const test5: HomomorphicPick<{readonly a: 1; b?: 2} | {readonly c?: 3; d?: 4}, 'a' | 'c'>;
20+
expectType<{readonly a: 1} | {readonly c?: 3}>(test5);
21+
22+
// Passes through primitives unchanged
23+
declare const test6: HomomorphicPick<string, never>;
24+
expectType<string>(test6);
25+
26+
declare const test7: HomomorphicPick<number, never>;
27+
expectType<number>(test7);
28+
29+
declare const test8: HomomorphicPick<boolean, never>;
30+
expectType<boolean>(test8);
31+
32+
declare const test9: HomomorphicPick<bigint, never>;
33+
expectType<bigint>(test9);
34+
35+
declare const test10: HomomorphicPick<symbol, never>;
36+
expectType<symbol>(test10);
37+
38+
// Picks all keys, if `KeyType` is `any`
39+
declare const test11: HomomorphicPick<{readonly a: 1; b?: 2} | {readonly c?: 3}, any>;
40+
expectType<{readonly a: 1; b?: 2} | {readonly c?: 3}>(test11);
41+
42+
// Picks no keys, if `KeyType` is `never`
43+
declare const test12: HomomorphicPick<{a: 1; b: 2}, never>;
44+
expectType<{}>(test12);
45+
46+
// Works with index signatures
47+
declare const test13: HomomorphicPick<{[k: string]: unknown; a: 1; b: 2}, 'a' | 'b'>;
48+
expectType<{a: 1; b: 2}>(test13);
49+
50+
// Doesn't pick `number` from a `string` index signature
51+
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
52+
declare const test14: HomomorphicPick<{[k: string]: unknown}, number>;
53+
expectType<{}>(test14);

test-d/set-optional.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ expectType<{readonly a?: number; b?: string; c?: boolean}>(variation7);
3232
// Does nothing, if `Keys` is `never`.
3333
declare const variation8: SetOptional<{a?: number; readonly b?: string; readonly c: boolean}, never>;
3434
expectType<{a?: number; readonly b?: string; readonly c: boolean}>(variation8);
35+
36+
// Works with index signatures
37+
declare const variation9: SetOptional<{[k: string]: unknown; a: number; b?: string}, 'a' | 'b'>;
38+
expectType<{[k: string]: unknown; a?: number; b?: string}>(variation9);

test-d/set-readonly.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ expectType<{readonly a?: number; readonly b: string; readonly c: boolean}>(varia
3232
// Does nothing, if `Keys` is `never`.
3333
declare const variation8: SetReadonly<{a: number; readonly b: string; readonly c: boolean}, never>;
3434
expectType<{a: number; readonly b: string; readonly c: boolean}>(variation8);
35+
36+
// Works with index signatures
37+
declare const variation9: SetReadonly<{[k: string]: unknown; a: number; readonly b: string}, 'a' | 'b'>;
38+
expectType<{[k: string]: unknown; readonly a: number; readonly b: string}>(variation9);

test-d/set-required.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,7 @@ expectType<{readonly a: number; b: string; c: boolean}>(variation8);
3636
// Does nothing, if `Keys` is `never`.
3737
declare const variation9: SetRequired<{a?: number; readonly b?: string; readonly c: boolean}, never>;
3838
expectType<{a?: number; readonly b?: string; readonly c: boolean}>(variation9);
39+
40+
// Works with index signatures
41+
declare const variation10: SetRequired<{[k: string]: unknown; a?: number; b: string}, 'a' | 'b'>;
42+
expectType<{[k: string]: unknown; a: number; b: string}>(variation10);

0 commit comments

Comments
 (0)