-
-
Notifications
You must be signed in to change notification settings - Fork 567
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sindre Sorhus <[email protected]>
- Loading branch information
1 parent
8f3a1a5
commit fa4099c
Showing
4 changed files
with
164 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |