Skip to content

Commit

Permalink
Add SharedUnionFieldsDeep type (#783)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
Emiyaaaaa and sindresorhus authored Dec 27, 2023
1 parent 00233da commit 94bb3d3
Show file tree
Hide file tree
Showing 6 changed files with 432 additions and 37 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export type {ArrayIndices} from './source/array-indices';
export type {ArrayValues} from './source/array-values';
export type {SetFieldType} from './source/set-field-type';
export type {Paths} from './source/paths';
export type {SharedUnionFieldsDeep} from './source/shared-union-fields-deep';

// Template literal types
export type {CamelCase} from './source/camel-case';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ Click the type names for complete docs.
- [`ArrayValues`](source/array-values.d.ts) - Provides all values for a constant array or tuple.
- [`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.

### Type Guard

Expand Down
112 changes: 110 additions & 2 deletions source/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {Simplify} from './simplify';
import type {Trim} from './trim';
import type {IsAny} from './is-any';
import type {UnknownRecord} from './unknown-record';
import type {IsNever} from './is-never';
import type {UnknownArray} from './unknown-array';

// TODO: Remove for v5.
Expand All @@ -13,7 +14,34 @@ Infer the length of the given array `<T>`.
@link https://itnext.io/implementing-arithmetic-within-typescripts-type-system-a1ef140a6f6f
*/
type TupleLength<T extends readonly unknown[]> = T extends {readonly length: infer L} ? L : never;
type ArrayLength<T extends readonly unknown[]> = T extends {readonly length: infer L} ? L : never;

/**
Infer the length of the given tuple `<T>`.
Returns `never` if the given type is an non-fixed-length array like `Array<string>`.
@example
```
type Tuple = TupleLength<[string, number, boolean]>;
//=> 3
type Array = TupleLength<string[]>;
//=> never
// Supports union types.
type Union = TupleLength<[] | [1, 2, 3] | Array<number>>;
//=> 1 | 3
```
*/
export type TupleLength<T extends UnknownArray> =
// `extends unknown` is used to convert `T` (if `T` is a union type) to
// a [distributive conditionaltype](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types))
T extends unknown
? number extends T['length']
? never // Return never if the given type is an non-flexed-length array like `Array<string>`
: T['length']
: never; // Should never happen

/**
Create a tuple type of the given length `<L>` and fill it with the given type `<Fill>`.
Expand Down Expand Up @@ -64,7 +92,7 @@ the inferred tuple `U` and a tuple of length `B`, then extracts the length of tu
@link https://itnext.io/implementing-arithmetic-within-typescripts-type-system-a1ef140a6f6f
*/
export type Subtract<A extends number, B extends number> = BuildTuple<A> extends [...(infer U), ...BuildTuple<B>]
? TupleLength<U>
? ArrayLength<U>
: never;

/**
Expand Down Expand Up @@ -328,3 +356,83 @@ IsPrimitive<Object>
```
*/
export type IsPrimitive<T> = [T] extends [Primitive] ? true : false;

/**
Returns the static, fixed-length portion of the given array, excluding variable-length parts.
@example
```
type A = [string, number, boolean, ...string[]];
type B = StaticPartOfArray<A>;
//=> [string, number, boolean]
```
*/
export type StaticPartOfArray<T extends UnknownArray, Result extends UnknownArray = []> =
T extends unknown
? number extends T['length'] ?
T extends readonly [infer U, ...infer V]
? StaticPartOfArray<V, [...Result, U]>
: Result
: T
: never; // Should never happen

/**
Returns the variable, non-fixed-length portion of the given array, excluding static-length parts.
@example
```
type A = [string, number, boolean, ...string[]];
type B = VariablePartOfArray<A>;
//=> string[]
```
*/
export type VariablePartOfArray<T extends UnknownArray> =
T extends unknown
? T extends readonly [...StaticPartOfArray<T>, ...infer U]
? U
: []
: never; // Should never happen

/**
Returns the minimum number in the given union of numbers.
Note: Just supports numbers from 0 to 999.
@example
```
type A = UnionMin<3 | 1 | 2>;
//=> 1
```
*/
export type UnionMin<N extends number> = InternalUnionMin<N>;

/**
The actual implementation of `UnionMin`. It's private because it has some arguments that don't need to be exposed.
*/
type InternalUnionMin<N extends number, T extends UnknownArray = []> =
T['length'] extends N
? T['length']
: InternalUnionMin<N, [...T, unknown]>;

/**
Returns the maximum number in the given union of numbers.
Note: Just supports numbers from 0 to 999.
@example
```
type A = UnionMax<1 | 3 | 2>;
//=> 3
```
*/
export type UnionMax<N extends number> = InternalUnionMax<N>;

/**
The actual implementation of `UnionMax`. It's private because it has some arguments that don't need to be exposed.
*/
type InternalUnionMax<N extends number, T extends UnknownArray = []> =
IsNever<N> extends true
? T['length']
: T['length'] extends N
? InternalUnionMax<Exclude<N, T['length']>, T>
: InternalUnionMax<N, [...T, unknown]>;
38 changes: 3 additions & 35 deletions source/paths.d.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,9 @@
import type {NonRecursiveType, ToString} from './internal';
import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString} from './internal';
import type {EmptyObject} from './empty-object';
import type {IsAny} from './is-any';
import type {IsNever} from './is-never';
import type {UnknownArray} from './unknown-array';

/**
Return the part of the given array with a fixed index.
@example
```
type A = [string, number, boolean, ...string[]];
type B = FilterFixedIndexArray<A>;
//=> [string, number, boolean]
```
*/
type FilterFixedIndexArray<T extends UnknownArray, Result extends UnknownArray = []> =
number extends T['length'] ?
T extends readonly [infer U, ...infer V]
? FilterFixedIndexArray<V, [...Result, U]>
: Result
: T;

/**
Return the part of the given array with a non-fixed index.
@example
```
type A = [string, number, boolean, ...string[]];
type B = FilterNotFixedIndexArray<A>;
//=> string[]
```
*/
type FilterNotFixedIndexArray<T extends UnknownArray> =
T extends readonly [...FilterFixedIndexArray<T>, ...infer U]
? U
: [];

/**
Generate a union of all possible paths to properties in the given object.
Expand Down Expand Up @@ -85,8 +53,8 @@ export type Paths<T> =
: T extends UnknownArray
? number extends T['length']
// We need to handle the fixed and non-fixed index part of the array separately.
? InternalPaths<FilterFixedIndexArray<T>>
| InternalPaths<Array<FilterNotFixedIndexArray<T>[number]>>
? InternalPaths<StaticPartOfArray<T>>
| InternalPaths<Array<VariablePartOfArray<T>[number]>>
: InternalPaths<T>
: T extends object
? InternalPaths<T>
Expand Down
192 changes: 192 additions & 0 deletions source/shared-union-fields-deep.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import type {NonRecursiveType, UnionMin, UnionMax, TupleLength, StaticPartOfArray, VariablePartOfArray} from './internal';
import type {IsNever} from './is-never';
import type {UnknownArray} from './unknown-array';

/**
Set the given array to readonly if `IsReadonly` is `true`, otherwise set the given array to normal, then return the result.
@example
```
type ReadonlyArray = readonly string[];
type NormalArray = string[];
type ReadonlyResult = SetArrayAccess<NormalArray, true>;
//=> readonly string[]
type NormalResult = SetArrayAccess<ReadonlyArray, false>;
//=> string[]
```
*/
type SetArrayAccess<T extends UnknownArray, IsReadonly extends boolean> =
T extends readonly [...infer U] ?
IsReadonly extends true
? readonly [...U]
: [...U]
: T;

/**
Returns whether the given array `T` is readonly.
*/
type IsArrayReadonly<T extends UnknownArray> = T extends unknown[] ? false : true;

/**
SharedUnionFieldsDeep options.
@see {@link SharedUnionFieldsDeep}
*/
export type SharedUnionFieldsDeepOptions = {
/**
When set to true, this option impacts each element within arrays or tuples. If all union values are arrays or tuples, it constructs an array of the shortest possible length, ensuring every element exists in the union array.
@default false
*/
recurseIntoArrays?: boolean;
};

/**
Create a type with shared fields from a union of object types, deeply traversing nested structures.
Use the {@link SharedUnionFieldsDeepOptions `Options`} to specify the behavior for arrays.
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 {SharedUnionFieldsDeep} from 'type-fest';
type Cat = {
info: {
name: string;
type: 'cat';
catType: string;
};
};
type Dog = {
info: {
name: string;
type: 'dog';
dogType: string;
};
};
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: '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: SharedUnionFieldsDeep<Cat | Dog>['info']) {
// typeof petInfo =>
// {
// name: string;
// type: 'cat' | 'dog';
// }
// petInfo type is simple and clear
console.log('name: ', petInfo.name);
console.log('type: ', petInfo.type);
}
```
@category Object
@category Union
*/
export type SharedUnionFieldsDeep<Union, Options extends SharedUnionFieldsDeepOptions = {recurseIntoArrays: false}> =
// `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>]
? Union
: [Union] extends [UnknownArray]
? Options['recurseIntoArrays'] extends true
? SetArrayAccess<SharedArrayUnionFieldsDeep<Union, Options>, IsArrayReadonly<Union>>
: Union
: [Union] extends [object]
? SharedObjectUnionFieldsDeep<Union, Options>
: Union;

/**
Same as `SharedUnionFieldsDeep`, but accepts only `object`s and as inputs. Internal helper for `SharedUnionFieldsDeep`.
*/
type SharedObjectUnionFieldsDeep<Union, Options extends SharedUnionFieldsDeepOptions> =
keyof Union extends infer Keys
? IsNever<Keys> extends false
? {
[Key in keyof Union]:
Union[Key] extends NonRecursiveType
? Union[Key]
: SharedUnionFieldsDeep<Union[Key], Options>
}
: {}
: Union;

/**
Same as `SharedUnionFieldsDeep`, but accepts only `UnknownArray`s and as inputs. Internal helper for `SharedUnionFieldsDeep`.
*/
type SharedArrayUnionFieldsDeep<Union extends UnknownArray, Options extends SharedUnionFieldsDeepOptions> =
// Restore the readonly modifier of the array.
SetArrayAccess<
InternalSharedArrayUnionFieldsDeep<Union, Options>,
IsArrayReadonly<Union>
>;

/**
Internal helper for `SharedArrayUnionFieldsDeep`. Needn't care the `readonly` modifier of arrays.
*/
type InternalSharedArrayUnionFieldsDeep<
Union extends UnknownArray,
Options extends SharedUnionFieldsDeepOptions,
ResultTuple extends UnknownArray = [],
> =
// We should build a minimum possible length tuple where each element in the tuple exists in the union tuple.
IsNever<TupleLength<Union>> extends true
// Rule 1: If all the arrays in the union have non-fixed lengths,
// like `Array<string> | [number, ...string[]]`
// we should build a tuple that is [the_fixed_parts_of_union, ...the_rest_of_union[]].
// For example: `InternalSharedArrayUnionFieldsDeep<Array<string> | [number, ...string[]]>`
// => `[string | number, ...string[]]`.
? ResultTuple['length'] extends UnionMax<StaticPartOfArray<Union>['length']>
? [
// The fixed-length part of the tuple.
...ResultTuple,
// The rest of the union.
// Due to `ResultTuple` is the maximum possible fixed-length part of the tuple,
// so we can use `StaticPartOfArray` to get the rest of the union.
...Array<
SharedUnionFieldsDeep<VariablePartOfArray<Union>[number], Options>
>,
]
// Build the fixed-length tuple recursively.
: InternalSharedArrayUnionFieldsDeep<
Union, Options,
[...ResultTuple, SharedUnionFieldsDeep<Union[ResultTuple['length']], Options>]
>
// Rule 2: If at least one of the arrays in the union have fixed lengths,
// like `Array<string> | [number, string]`,
// we should build a tuple of the smallest possible length to ensure any
// item in the result tuple exists in the union tuple.
// For example: `InternalSharedArrayUnionFieldsDeep<Array<string> | [number, string]>`
// => `[string | number, string]`.
: ResultTuple['length'] extends UnionMin<TupleLength<Union>>
? ResultTuple
// As above, build tuple recursively.
: InternalSharedArrayUnionFieldsDeep<
Union, Options,
[...ResultTuple, SharedUnionFieldsDeep<Union[ResultTuple['length']], Options>]
>;
Loading

0 comments on commit 94bb3d3

Please sign in to comment.