-
-
Notifications
You must be signed in to change notification settings - Fork 542
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
2bc451e
commit bc49577
Showing
4 changed files
with
168 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,89 @@ | ||
import type {KeysOfUnion} from './keys-of-union'; | ||
|
||
/** | ||
Omits keys from a type, distributing the operation over a union. | ||
TypeScript's `Omit` doesn't distribute over unions, leading to the erasure of unique properties from union members when omitting keys. This creates a type that only retains properties common to all union members, making it impossible to access member-specific properties after the Omit. Essentially, using `Omit` 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: string; | ||
a: number; | ||
}; | ||
type B = { | ||
discriminant: 'B'; | ||
foo: string; | ||
b: string; | ||
}; | ||
type Union = A | B; | ||
type OmittedUnion = Omit<Union, 'foo'>; | ||
//=> {discriminant: 'A' | 'B'} | ||
const omittedUnion: OmittedUnion = createOmittedUnion(); | ||
if (omittedUnion.discriminant === 'A') { | ||
// We would like to narrow `omittedUnion`'s type | ||
// to `A` here, but we can't because `Omit` | ||
// doesn't distribute over unions. | ||
omittedUnion.a; | ||
//=> Error: `a` is not a property of `{discriminant: 'A' | 'B'}` | ||
} | ||
``` | ||
While `Except` solves this problem, it restricts the keys you can omit to the ones that are present in **ALL** union members, where `DistributedOmit` allows you to omit keys that are present in **ANY** union member. | ||
@example | ||
``` | ||
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; | ||
}; | ||
// Notice that `foo` exists in `A` and `B`, but not in `C`, and | ||
// `bar` exists in `B` and `C`, but not in `A`. | ||
type Union = A | B | C; | ||
type OmittedUnion = DistributedOmit<Union, 'foo' | 'bar'>; | ||
const omittedUnion: OmittedUnion = createOmittedUnion(); | ||
if (omittedUnion.discriminant === 'A') { | ||
omittedUnion.a; | ||
//=> OK | ||
omittedUnion.foo; | ||
//=> Error: `foo` is not a property of `{discriminant: 'A'; a: string}` | ||
omittedUnion.bar; | ||
//=> Error: `bar` is not a property of `{discriminant: 'A'; a: string}` | ||
} | ||
``` | ||
@category Object | ||
*/ | ||
export type DistributedOmit<ObjectType, KeyType extends KeysOfUnion<ObjectType>> = | ||
ObjectType extends unknown | ||
? Omit<ObjectType, KeyType> | ||
: 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 {DistributedOmit, Except} from '../index'; | ||
|
||
// When passing a non-union type, and | ||
// omitting keys that are present in the type. | ||
// It behaves exactly like `Except`. | ||
|
||
type Example1 = { | ||
a: number; | ||
b: string; | ||
}; | ||
|
||
type Actual1 = DistributedOmit<Example1, 'a'>; | ||
type Actual2 = DistributedOmit<Example1, 'b'>; | ||
type Actual3 = DistributedOmit<Example1, 'a' | 'b'>; | ||
|
||
type Expected1 = Except<Example1, 'a'>; | ||
type Expected2 = Except<Example1, 'b'>; | ||
type Expected3 = Except<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 | ||
// omitting keys that are NOT present in the type. | ||
// It behaves exactly like `Except`, by not letting you | ||
// omit keys that are not present in the type. | ||
|
||
type Example2 = { | ||
a: number; | ||
b: string; | ||
}; | ||
|
||
expectError(() => { | ||
type Actual4 = DistributedOmit<Example2, 'c'>; | ||
}); | ||
|
||
// When passing a union type, and | ||
// omitting keys that are present in some union members. | ||
// It lets you omit 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 OmittedUnion = DistributedOmit<Union, 'foo' | 'bar'>; | ||
|
||
declare const omittedUnion: OmittedUnion; | ||
|
||
if (omittedUnion.discriminant === 'A') { | ||
expectType<{discriminant: 'A'; a: number}>(omittedUnion); | ||
expectError(omittedUnion.foo); | ||
expectError(omittedUnion.bar); | ||
} |