Skip to content

Commit

Permalink
Add SharedUnionFieldsDeep type
Browse files Browse the repository at this point in the history
  • Loading branch information
Emiyaaaaa committed Dec 15, 2023
1 parent ebb7a59 commit 67112a4
Show file tree
Hide file tree
Showing 6 changed files with 425 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 deep version of anther object that has the shared fields of the given union object type.

### Type Guard

Expand Down
102 changes: 100 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,73 @@ IsPrimitive<Object>
```
*/
export type IsPrimitive<T> = [T] extends [Primitive] ? true : false;

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

/**
Returns the non-fixed-indexed part of the given array.
@example
```
type A = [string, number, boolean, ...string[]];
type B = NonFixedPartOfArray<A>;
//=> string[]
```
*/
export type NonFixedPartOfArray<T extends UnknownArray> =
T extends unknown
? T extends readonly [...FixedPartOfArray<T>, ...infer U]
? U
: []
: never; // Should never happen

/**
Returns the minimum number of the given union numbers.
Note: Just supports numbers from 0 to 999.
@example
```
type A = MinNumbers<3 | 1 | 2>
//=> 1
```
*/
export type MinNumbers<N extends Number, T extends UnknownArray = []> =
T['length'] extends N
? T['length']
: MinNumbers<N, [...T, unknown]>;

/**
Returns the maximum number of the given union numbers.
Note: Just supports numbers from 0 to 999.
@example
```
type A = MaxNumbers<1 | 3 | 2>
//=> 3
```
*/
export type MaxNumbers<N extends Number, T extends UnknownArray = []> =
IsNever<N> extends true
? T['length']
: T['length'] extends N
? MaxNumbers<Exclude<N, T['length']>, T>
: MaxNumbers<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 {FixedPartOfArray, NonFixedPartOfArray, 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<FixedPartOfArray<T>>
| InternalPaths<Array<NonFixedPartOfArray<T>[number]>>
: InternalPaths<T>
: T extends object
? InternalPaths<T>
Expand Down
195 changes: 195 additions & 0 deletions source/shared-union-fields-deep.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import type {NonRecursiveType, MinNumbers, MaxNumbers, TupleLength, FixedPartOfArray, NonFixedPartOfArray} 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 ArrayIsReadonly<T extends UnknownArray> = T extends unknown[] ? false : true;

/**
SharedUnionFieldsDeep options.
@see {@link SharedUnionFieldsDeep}
*/
export type SharedUnionFieldsDeepOptions = {
/**
Whether to affect the individual elements of arrays and tuples.
If this option is set to `true` and all of the value in union are arrays or tuples,
we will build a minimum possible length array that each element in the array is exist in the union array.
@default false
*/
recurseIntoArrays?: boolean;
};

/**
Create a deep version of anther object that has the shared fields of the given union object type.
uses the {@link SharedUnionFieldsDeepOptions `Options`} to specify the behavior of the array.
Use-cases:
- You want a safe object type that each key is exist in the union object.
- You want focus on the common fields of the union type and don't want 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>, ArrayIsReadonly<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>,
ArrayIsReadonly<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 that each element in the tuple is exist 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 MaxNumbers<FixedPartOfArray<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 `FixedPartOfArray` to get the rest of the union.
...Array<
SharedUnionFieldsDeep<NonFixedPartOfArray<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 is exist in the union tuple.
// For example: `InternalSharedArrayUnionFieldsDeep<Array<string> | [number, string]>`
// => `[string | number, string]`.
: ResultTuple['length'] extends MinNumbers<TupleLength<Union>>
? ResultTuple
// As above, build tuple recursively.
: InternalSharedArrayUnionFieldsDeep<
Union, Options,
[...ResultTuple, SharedUnionFieldsDeep<Union[ResultTuple['length']], Options>]
>;
Loading

0 comments on commit 67112a4

Please sign in to comment.