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 SharedUnionFields type #994

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export type {ArraySplice} from './source/array-splice';
export type {ArrayTail} from './source/array-tail';
export type {SetFieldType} from './source/set-field-type';
export type {Paths} from './source/paths';
export type {SharedUnionFields} from './source/shared-union-fields';
export type {SharedUnionFieldsDeep} from './source/shared-union-fields-deep';
export type {IsNull} from './source/is-null';
export type {IfNull} from './source/if-null';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ Click the type names for complete docs.
- [`ArrayTail`](source/array-tail.d.ts) - Extracts the type of an array or tuple minus the first element.
- [`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.
- [`SharedUnionFields`](source/shared-union-fields.d.ts) - Create a type with shared fields from a union of object types.
- [`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.
Expand Down
18 changes: 10 additions & 8 deletions source/shared-union-fields-deep.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ type Dog = {
function displayPetInfo(petInfo: (Cat | Dog)['info']) {
// typeof petInfo =>
// {
// name: string;
// type: 'cat';
// catType: string; // Needn't care about this field, because it's not a common pet info field.
// name: string;
// type: 'cat';
// catType: string; // Needn't care about this field, because it's not a common pet info field.
// } | {
// name: string;
// type: 'dog';
// dogType: string; // Needn't care about this field, because it's not a common pet info field.
// name: string;
// type: 'dog';
// dogType: string; // Needn't care about this field, because it's not a common pet info field.
// }

// petInfo type is complex and have some needless fields
Expand All @@ -66,8 +66,8 @@ function displayPetInfo(petInfo: (Cat | Dog)['info']) {
function displayPetInfo(petInfo: SharedUnionFieldsDeep<Cat | Dog>['info']) {
// typeof petInfo =>
// {
// name: string;
// type: 'cat' | 'dog';
// name: string;
// type: 'cat' | 'dog';
// }

// petInfo type is simple and clear
Expand All @@ -77,6 +77,8 @@ function displayPetInfo(petInfo: SharedUnionFieldsDeep<Cat | Dog>['info']) {
}
```

@see SharedUnionFields

@category Object
@category Union
*/
Expand Down
83 changes: 83 additions & 0 deletions source/shared-union-fields.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type {NonRecursiveType, IsUnion} from './internal';
import type {IsNever} from './is-never';
import type {UnknownArray} from './unknown-array';

/**
Create a type with shared fields from a union of object types.

Use-cases:
- You want a safe object type where each key exists in the union object.
- You want to focus on the common fields of the union type and don't want to have to care about the other fields.

@example
```
import type {SharedUnionFields} from 'type-fest';

type Cat = {
name: string;
type: 'cat';
catType: string;
};

type Dog = {
name: string;
type: 'dog';
dogType: string;
};

function displayPetInfo(petInfo: Cat | Dog) {
// typeof petInfo =>
// {
// name: string;
// type: 'cat';
// catType: string; // Needn't care about this field, because it's not a common pet info field.
// } | {
// name: string;
// type: 'dog';
// dogType: string; // Needn't care about this field, because it's not a common pet info field.
// }

// petInfo type is complex and have some needless fields

console.log('name: ', petInfo.name);
console.log('type: ', petInfo.type);
}

function displayPetInfo(petInfo: SharedUnionFields<Cat | Dog>) {
// typeof petInfo =>
// {
// name: string;
// type: 'cat' | 'dog';
// }

// petInfo type is simple and clear

console.log('name: ', petInfo.name);
console.log('type: ', petInfo.type);
}
```

@see SharedUnionFieldsDeep

@category Object
@category Union
*/
export type SharedUnionFields<Union> =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of this type could be refactored to simply this:

export type SharedUnionFields<Union> = [Union] extends [
	NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | UnknownArray,
]
	? Union
	: Pick<Union, keyof Union>;

This seems to handle all the scenarios.

// If `Union` is not a union type, return `Union` directly.
IsUnion<Union> extends false
? Union
// `Union extends` will convert `Union`
// to a [distributive conditionaltype](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types).
// But this is not what we want, so we need to wrap `Union` with `[]` to prevent it.
: [Union] extends [NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | UnknownArray]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, there's no test case that verifies this conditional [Union] extends [NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | UnknownArray]. All tests continue to pass even if the conditional is removed.


A test like the following is needed to verify the above conditional:

expectType<Map<string, string> | Set<string>>({} as SharedUnionFields<Map<string, string> | Set<string>>);

Copy link
Collaborator

@som-sm som-sm Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, is this the intended behaviour?

type T = SharedUnionFields<RegExp | {test: 1} | {test: 2}>;

T is currently of the following type:

{ test: 1 | 2 | ((string: string) => boolean); }

Feels like one of these behaviours might be better than the existing behaviour in scenarios like these:

  1. We simply return back the input type, without any manipulation, like we'd do for RegExp | Date.
    RegExp | {test: 1} | {test: 2}
  2. Or, we can leave RegExp as is, and resolve the remaining members
    RegExp | {test: 1 | 2}

@Emiyaaaaa WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Or, we can leave RegExp as is, and resolve the remaining members
    RegExp | {test: 1 | 2}

This is better

? Union
: [Union] extends [object]
// `keyof Union` can extract the same key in union type, if there is no same key, return never.
? keyof Union extends infer Keys
? IsNever<Keys> extends false
? {
[Key in keyof Union]: Union[Key]
}
: {}
: Union
: Union;
84 changes: 84 additions & 0 deletions test-d/shared-union-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {expectType} from 'tsd';
import type {SharedUnionFields} from '../index';

type TestingType = {
function: (() => void);
record: Record<string, {
propertyA: string;
}>;
object: {
subObject: {
subSubObject: {
propertyA: string;
};
};
};
string: string;
union: 'test1' | 'test2';
number: number;
boolean: boolean;
date: Date;
regexp: RegExp;
symbol: symbol;
null: null;
undefined: undefined;
optional?: boolean | undefined;
readonly propertyWithKeyword: boolean;
map: Map<string, {propertyA: string; propertyB: string}>;
set: Set<string> ;
objectSet: Set<{propertyA: string; propertyB: string}>;
};

declare const normal: SharedUnionFields<TestingType | {string: string; number: number; foo: any}>;
expectType<{string: string; number: number}>(normal);

declare const normal2: SharedUnionFields<TestingType | {string: string; foo: any}>;
expectType<{string: string}>(normal2);

declare const unMatched: SharedUnionFields<TestingType | {foo: any}>;
expectType<{}>(unMatched);

declare const number: SharedUnionFields<TestingType | {number: number; foo: any}>;
expectType<{number: number}>(number);

declare const string: SharedUnionFields<TestingType | {string: string; foo: any}>;
expectType<{string: string}>(string);

declare const boolean: SharedUnionFields<TestingType | {boolean: boolean; foo: any}>;
expectType<{boolean: boolean}>(boolean);

declare const date: SharedUnionFields<TestingType | {date: Date; foo: any}>;
expectType<{date: Date}>(date);

declare const regexp: SharedUnionFields<TestingType | {regexp: RegExp; foo: any}>;
expectType<{regexp: RegExp}>(regexp);

declare const symbol: SharedUnionFields<TestingType | {symbol: symbol; foo: any}>;
expectType<{symbol: symbol}>(symbol);

declare const null_: SharedUnionFields<TestingType | {null: null; foo: any}>;
expectType<{null: null}>(null_);

declare const undefined_: SharedUnionFields<TestingType | {undefined: undefined; foo: any}>;
expectType<{undefined: undefined}>(undefined_);

declare const optional: SharedUnionFields<TestingType | {optional: string; foo: any}>;
expectType<{optional?: boolean | string | undefined}>(optional);

declare const propertyWithKeyword: SharedUnionFields<TestingType | {readonly propertyWithKeyword: string; foo: any}>;
expectType<{readonly propertyWithKeyword: boolean | string}>(propertyWithKeyword);

declare const map: SharedUnionFields<TestingType | {map: Map<string, {propertyA: string}>; foo: any}>;
expectType<{map: TestingType['map'] | Map<string, {propertyA: string}>}>(map);

declare const set: SharedUnionFields<TestingType | {set: Set<number>; foo: any}>;
expectType<{set: TestingType['set'] | Set<number>}>(set);

declare const moreUnion: SharedUnionFields<TestingType | {string: string; number: number; foo: any} | {string: string; bar: any}>;
expectType<{string: string}>(moreUnion);

declare const union: SharedUnionFields<TestingType | {union: {a: number}}>;
expectType<{union: 'test1' | 'test2' | {a: number}}>(union);

declare const unionWithOptional: SharedUnionFields<{a?: string; foo: number} | {a: string; bar: string}>;
expectType<{a?: string}>(unionWithOptional);