Skip to content

Commit

Permalink
fix: add new internal homomorphic-pick type and revert distributed-pi…
Browse files Browse the repository at this point in the history
…ck changes
  • Loading branch information
som-sm committed Dec 19, 2024
1 parent 9d7471f commit 19d58e3
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 11 deletions.
4 changes: 3 additions & 1 deletion source/distributed-pick.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,6 @@ if (pickedUnion.discriminant === 'A') {
@category Object
*/
export type DistributedPick<ObjectType, KeyType extends KeysOfUnion<ObjectType>> =
{[Key in keyof ObjectType as Extract<Key, KeyType>]: ObjectType[Key]};
ObjectType extends unknown
? Pick<ObjectType, Extract<KeyType, keyof ObjectType>>
: never;
42 changes: 42 additions & 0 deletions source/internal/object.d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -80,3 +81,44 @@ export type UndefinedToOptional<T extends object> = Simplify<
[Key in keyof Pick<T, FilterOptionalKeys<T>>]?: Exclude<T[Key], undefined>;
}
>;

/**
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<ImageUpload | VideoUpload, "url" | "size" | "duration">;
//=> {url: string; size: number} | {url: string; duration: number}
// Primitive types are returned as-is
type Primitive = HomomorphicPick<string | number, 'toUpperCase' | 'toString'>;
//=> 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<T, Keys extends KeysOfUnion<T>> = {
[P in keyof T as Extract<P, Keys>]: T[P]
};
4 changes: 2 additions & 2 deletions source/set-optional.d.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -34,6 +34,6 @@ export type SetOptional<BaseType, Keys extends keyof BaseType> =
// Pick just the keys that are readonly from the base type.
Except<BaseType, Keys> &
// Pick the keys that should be mutable from the base type and make them mutable.
Partial<DistributedPick<BaseType, Keys & KeysOfUnion<BaseType>>>
Partial<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
>
: never;
4 changes: 2 additions & 2 deletions source/set-readonly.d.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -35,6 +35,6 @@ export type SetReadonly<BaseType, Keys extends keyof BaseType> =
BaseType extends unknown
? Simplify<
Except<BaseType, Keys> &
Readonly<DistributedPick<BaseType, Keys & KeysOfUnion<BaseType>>>
Readonly<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
>
: never;
4 changes: 2 additions & 2 deletions source/set-required.d.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -37,6 +37,6 @@ export type SetRequired<BaseType, Keys extends keyof BaseType> =
// Pick just the keys that are optional from the base type.
Except<BaseType, Keys> &
// Pick the keys that should be required from the base type and make them required.
Required<DistributedPick<BaseType, Keys & KeysOfUnion<BaseType>>>
Required<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
>
: never;
4 changes: 0 additions & 4 deletions test-d/distributed-pick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
53 changes: 53 additions & 0 deletions test-d/internal/homomorphic-pick.ts
Original file line number Diff line number Diff line change
@@ -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<string, never>;
expectType<string>(test6);

declare const test7: HomomorphicPick<number, never>;
expectType<number>(test7);

declare const test8: HomomorphicPick<boolean, never>;
expectType<boolean>(test8);

declare const test9: HomomorphicPick<bigint, never>;
expectType<bigint>(test9);

declare const test10: HomomorphicPick<symbol, never>;
expectType<symbol>(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);

0 comments on commit 19d58e3

Please sign in to comment.