Skip to content

Commit

Permalink
Add DistributedOmit type (#820)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
henriqueinonhe and sindresorhus committed Mar 18, 2024
1 parent 2bc451e commit bc49577
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 0 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -6,6 +6,7 @@ export * from './source/observable-like';

// Utilities
export type {KeysOfUnion} from './source/keys-of-union';
export type {DistributedOmit} from './source/distributed-omit';
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 @@ -182,6 +182,7 @@ Click the type names for complete docs.
- [`SetFieldType`](source/set-field-type.d.ts) - Create a type that changes the type of the given keys.
- [`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.

### Type Guard

Expand Down
89 changes: 89 additions & 0 deletions source/distributed-omit.d.ts
@@ -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;
77 changes: 77 additions & 0 deletions test-d/distributed-omit.ts
@@ -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);
}

0 comments on commit bc49577

Please sign in to comment.