Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DistributedPick type #841

Merged
merged 3 commits into from Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -7,6 +7,7 @@ export * from './source/observable-like';
// Utilities
export type {KeysOfUnion} from './source/keys-of-union';
export type {DistributedOmit} from './source/distributed-omit';
export type {DistributedPick} from './source/distributed-pick';
export type {EmptyObject, IsEmptyObject} from './source/empty-object';
export type {NonEmptyObject} from './source/non-empty-object';
export type {UnknownRecord} from './source/unknown-record';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -183,6 +183,7 @@ Click the type names for complete docs.
- [`Paths`](source/paths.d.ts) - Generate a union of all possible paths to properties in the given object.
- [`SharedUnionFieldsDeep`](source/shared-union-fields-deep.d.ts) - Create a type with shared fields from a union of object types, deeply traversing nested structures.
- [`DistributedOmit`](source/distributed-omit.d.ts) - Omits keys from a type, distributing the operation over a union.
- [`DistributedPick`](source/distributed-pick.d.ts) - Picks keys from a type, distributing the operation over a union.

### Type Guard

Expand Down
85 changes: 85 additions & 0 deletions source/distributed-pick.d.ts
@@ -0,0 +1,85 @@
import type {KeysOfUnion} from './keys-of-union';

/**
Pick keys from a type, distributing the operation over a union.

TypeScript's `Pick` doesn't distribute over unions, leading to the erasure of unique properties from union members when picking keys. This creates a type that only retains properties common to all union members, making it impossible to access member-specific properties after the Pick. Essentially, using `Pick` on a union type merges the types into a less specific one, hindering type narrowing and property access based on discriminants. This type solves that.

Example:

```
type A = {
discriminant: 'A';
foo: {
bar: string;
};
};

type B = {
discriminant: 'B';
foo: {
baz: string;
};
};

type Union = A | B;

type PickedUnion = Pick<Union, 'discriminant' | 'foo'>;
//=> {discriminant: 'A' | 'B', foo: {bar: string} | {baz: string}}

const pickedUnion: PickedUnion = createPickedUnion();

if (pickedUnion.discriminant === 'A') {
// We would like to narrow `pickedUnion`'s type
// to `A` here, but we can't because `Pick`
// doesn't distribute over unions.

pickedUnion.foo.bar;
//=> Error: Property 'bar' does not exist on type '{bar: string} | {baz: string}'.
}
```

@example
```
type A = {
discriminant: 'A';
foo: {
bar: string;
};
extraneous: boolean;
};

type B = {
discriminant: 'B';
foo: {
baz: string;
};
extraneous: boolean;
};

// Notice that `foo.bar` exists in `A` but not in `B`.

type Union = A | B;

type PickedUnion = DistributedPick<Union, 'discriminant' | 'foo'>;

const pickedUnion: PickedUnion = createPickedUnion();

if (pickedUnion.discriminant === 'A') {
pickedUnion.foo.bar;
//=> OK

pickedUnion.extraneous;
//=> Error: Property `extraneous` does not exist on type `Pick<A, 'discriminant' | 'foo'>`.

pickedUnion.foo.baz;
//=> Error: `bar` is not a property of `{discriminant: 'A'; a: string}`.
}
```

@category Object
*/
export type DistributedPick<ObjectType, KeyType extends KeysOfUnion<ObjectType>> =
ObjectType extends unknown
? Pick<ObjectType, Extract<KeyType, keyof ObjectType>>
: never;
77 changes: 77 additions & 0 deletions test-d/distributed-pick.ts
@@ -0,0 +1,77 @@
import {expectType, expectError} from 'tsd';
import type {DistributedPick} from '../index';

// When passing a non-union type, and
// picking keys that are present in the type.
// It behaves exactly like `Pick`.

type Example1 = {
a: number;
b: string;
};

type Actual1 = DistributedPick<Example1, 'a'>;
type Actual2 = DistributedPick<Example1, 'b'>;
type Actual3 = DistributedPick<Example1, 'a' | 'b'>;

type Expected1 = Pick<Example1, 'a'>;
type Expected2 = Pick<Example1, 'b'>;
type Expected3 = Pick<Example1, 'a' | 'b'>;

declare const expected1: Expected1;
declare const expected2: Expected2;
declare const expected3: Expected3;

expectType<Actual1>(expected1);
expectType<Actual2>(expected2);
expectType<Actual3>(expected3);

// When passing a non-union type, and
// picking keys that are NOT present in the type.
// It behaves exactly like `Pick`, by not letting you
// pick keys that are not present in the type.

type Example2 = {
a: number;
b: string;
};

expectError(() => {
type Actual4 = DistributedPick<Example2, 'c'>;
});

// When passing a union type, and
// picking keys that are present in some union members.
// It lets you pick keys that are present in some union members,
// and distributes over the union.

type A = {
discriminant: 'A';
foo: string;
a: number;
};

type B = {
discriminant: 'B';
foo: string;
bar: string;
b: string;
};

type C = {
discriminant: 'C';
bar: string;
c: boolean;
};

type Union = A | B | C;

type PickedUnion = DistributedPick<Union, 'discriminant' | 'a' | 'b' | 'c'>;

declare const pickedUnion: PickedUnion;

if (pickedUnion.discriminant === 'A') {
expectType<{discriminant: 'A'; a: number}>(pickedUnion);
expectError(pickedUnion.foo);
expectError(pickedUnion.bar);
}