Skip to content

Commit bc49577

Browse files
Add DistributedOmit type (#820)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 2bc451e commit bc49577

4 files changed

Lines changed: 168 additions & 0 deletions

File tree

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './source/observable-like';
66

77
// Utilities
88
export type {KeysOfUnion} from './source/keys-of-union';
9+
export type {DistributedOmit} from './source/distributed-omit';
910
export type {EmptyObject, IsEmptyObject} from './source/empty-object';
1011
export type {NonEmptyObject} from './source/non-empty-object';
1112
export type {UnknownRecord} from './source/unknown-record';

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Click the type names for complete docs.
182182
- [`SetFieldType`](source/set-field-type.d.ts) - Create a type that changes the type of the given keys.
183183
- [`Paths`](source/paths.d.ts) - Generate a union of all possible paths to properties in the given object.
184184
- [`SharedUnionFieldsDeep`](source/shared-union-fields-deep.d.ts) - Create a type with shared fields from a union of object types, deeply traversing nested structures.
185+
- [`DistributedOmit`](source/distributed-omit.d.ts) - Omits keys from a type, distributing the operation over a union.
185186

186187
### Type Guard
187188

source/distributed-omit.d.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type {KeysOfUnion} from './keys-of-union';
2+
3+
/**
4+
Omits keys from a type, distributing the operation over a union.
5+
6+
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.
7+
8+
Example:
9+
10+
```
11+
type A = {
12+
discriminant: 'A';
13+
foo: string;
14+
a: number;
15+
};
16+
17+
type B = {
18+
discriminant: 'B';
19+
foo: string;
20+
b: string;
21+
};
22+
23+
type Union = A | B;
24+
25+
type OmittedUnion = Omit<Union, 'foo'>;
26+
//=> {discriminant: 'A' | 'B'}
27+
28+
const omittedUnion: OmittedUnion = createOmittedUnion();
29+
30+
if (omittedUnion.discriminant === 'A') {
31+
// We would like to narrow `omittedUnion`'s type
32+
// to `A` here, but we can't because `Omit`
33+
// doesn't distribute over unions.
34+
35+
omittedUnion.a;
36+
//=> Error: `a` is not a property of `{discriminant: 'A' | 'B'}`
37+
}
38+
```
39+
40+
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.
41+
42+
@example
43+
```
44+
type A = {
45+
discriminant: 'A';
46+
foo: string;
47+
a: number;
48+
};
49+
50+
type B = {
51+
discriminant: 'B';
52+
foo: string;
53+
bar: string;
54+
b: string;
55+
};
56+
57+
type C = {
58+
discriminant: 'C';
59+
bar: string;
60+
c: boolean;
61+
};
62+
63+
// Notice that `foo` exists in `A` and `B`, but not in `C`, and
64+
// `bar` exists in `B` and `C`, but not in `A`.
65+
66+
type Union = A | B | C;
67+
68+
type OmittedUnion = DistributedOmit<Union, 'foo' | 'bar'>;
69+
70+
const omittedUnion: OmittedUnion = createOmittedUnion();
71+
72+
if (omittedUnion.discriminant === 'A') {
73+
omittedUnion.a;
74+
//=> OK
75+
76+
omittedUnion.foo;
77+
//=> Error: `foo` is not a property of `{discriminant: 'A'; a: string}`
78+
79+
omittedUnion.bar;
80+
//=> Error: `bar` is not a property of `{discriminant: 'A'; a: string}`
81+
}
82+
```
83+
84+
@category Object
85+
*/
86+
export type DistributedOmit<ObjectType, KeyType extends KeysOfUnion<ObjectType>> =
87+
ObjectType extends unknown
88+
? Omit<ObjectType, KeyType>
89+
: never;

test-d/distributed-omit.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {expectType, expectError} from 'tsd';
2+
import type {DistributedOmit, Except} from '../index';
3+
4+
// When passing a non-union type, and
5+
// omitting keys that are present in the type.
6+
// It behaves exactly like `Except`.
7+
8+
type Example1 = {
9+
a: number;
10+
b: string;
11+
};
12+
13+
type Actual1 = DistributedOmit<Example1, 'a'>;
14+
type Actual2 = DistributedOmit<Example1, 'b'>;
15+
type Actual3 = DistributedOmit<Example1, 'a' | 'b'>;
16+
17+
type Expected1 = Except<Example1, 'a'>;
18+
type Expected2 = Except<Example1, 'b'>;
19+
type Expected3 = Except<Example1, 'a' | 'b'>;
20+
21+
declare const expected1: Expected1;
22+
declare const expected2: Expected2;
23+
declare const expected3: Expected3;
24+
25+
expectType<Actual1>(expected1);
26+
expectType<Actual2>(expected2);
27+
expectType<Actual3>(expected3);
28+
29+
// When passing a non-union type, and
30+
// omitting keys that are NOT present in the type.
31+
// It behaves exactly like `Except`, by not letting you
32+
// omit keys that are not present in the type.
33+
34+
type Example2 = {
35+
a: number;
36+
b: string;
37+
};
38+
39+
expectError(() => {
40+
type Actual4 = DistributedOmit<Example2, 'c'>;
41+
});
42+
43+
// When passing a union type, and
44+
// omitting keys that are present in some union members.
45+
// It lets you omit keys that are present in some union members,
46+
// and distributes over the union.
47+
48+
type A = {
49+
discriminant: 'A';
50+
foo: string;
51+
a: number;
52+
};
53+
54+
type B = {
55+
discriminant: 'B';
56+
foo: string;
57+
bar: string;
58+
b: string;
59+
};
60+
61+
type C = {
62+
discriminant: 'C';
63+
bar: string;
64+
c: boolean;
65+
};
66+
67+
type Union = A | B | C;
68+
69+
type OmittedUnion = DistributedOmit<Union, 'foo' | 'bar'>;
70+
71+
declare const omittedUnion: OmittedUnion;
72+
73+
if (omittedUnion.discriminant === 'A') {
74+
expectType<{discriminant: 'A'; a: number}>(omittedUnion);
75+
expectError(omittedUnion.foo);
76+
expectError(omittedUnion.bar);
77+
}

0 commit comments

Comments
 (0)