Utility Types for Properties#
type Partial<T> = {
[P in keyof T]?: T[P];
};
type DeepPartial<T extends object> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type DeepRequired<T extends object> = {
[K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type DeepReadonly<T extends object> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type DeepMutable<T extends object> = {
-readonly [K in keyof T]: T[K] extends object ? DeepMutable<T[K]> : T[K];
};
type NonNullable<T> = T extends null | undefined ? never : T;
export type DeepNonNullable<T extends object> = {
[K in keyof T]: T[K] extends object
? DeepNonNullable<T[K]>
: NonNullable<T[K]>;
};
type Nullable<T> = T | null;
type DeepNullable<T extends object> = {
[K in keyof T]: T[K] extends object ? DeepNullable<T[K]> : Nullable<T[K]>;
};
Decomposing complex utility types into combinations of basic utility types (Split-Process-Combine)
type Flatten<T> = { [K in keyof T]: T[K] };
type MarkPropsAsOptional<
T extends object,
K extends keyof T = keyof T
> = Flatten<Partial<Pick<T, K>> & Omit<T, K>>;
type MarkPropsAsRequired<
T extends object,
K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Required<Pick<T, K>>>;
type MarkPropsAsReadonly<
T extends object,
K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Readonly<Pick<T, K>>>;
type MarkPropsAsMutable<
T extends object,
K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Mutable<Pick<T, K>>>;
type MarkPropsAsNullable<
T extends object,
K extends keyof T = keyof T
> = Flatten<Omit<T, K> & Nullable<Pick<T, K>>>;
type MarkPropsAsNonNullable<
T extends object,
K extends keyof T = keyof T
> = Flatten<Omit<T, K> & NonNullable<Pick<T, K>>>;
Structural Utility Types#
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Dictionary<T> = {
[index: string]: T;
};
type NumericDictionary<T> = {
[index: number]: T;
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
The result of Exclude<A, B>
is the part of the union type A that does not exist in B.
Pick and Omit Based on Value Types#
type ExpectedPropKeys<T extends object, ValueType> = {
[Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];
type PickByValueType<T extends object, ValueType> = Pick<
T,
ExpectedPropKeys<T, ValueType>
>;
type FilteredPropKeys<T extends object, ValueType> = {
[Key in keyof T]-?: T[Key] extends ValueType ? never : Key;
}[keyof T];
type OmitByValueType<T extends object, ValueType> = Pick<
T,
FilteredPropKeys<T, ValueType>
>;
type StrictConditional<A, B, Resolved, Rejected, Fallback = never> = [
A
] extends [B]
? [B] extends [A]
? Resolved
: Rejected
: Fallback;
type StrictValueTypeFilter<
T extends object,
ValueType,
Positive extends boolean = true
> = {
[Key in keyof T]-?: StrictConditional<
ValueType,
T[Key],
// To avoid too many nested utility types, we won't use Conditional here
Positive extends true ? Key : never,
Positive extends true ? never : Key,
Positive extends true ? never : Key
>;
}[keyof T];
type StrictPickByValueType<T extends object, ValueType> = Pick<
T,
StrictValueTypeFilter<T, ValueType>
>;
type StrictOmitByValueType<T extends object, ValueType> = Pick<
T,
StrictValueTypeFilter<T, ValueType, false>
>;
Mutually Exclusive Handling of Substructures#
Imagine a scenario where we have an object structure describing user information. In addition to some common basic structures, VIP users, regular users, and visitors each have some unique fields, such as vipExpires representing the VIP expiration time, which belongs only to VIP users, promotionUsed representing the claimed experience coupon, which belongs to regular users, and refererType representing the source of the jump, which belongs to visitors.
interface VIP {
vipExpires: number;
}
interface CommonUser {
promotionUsed: boolean;
}
interface Visitor {
refererType: RefererType;
}
type User = VIP | CommonUser;
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T);
type XORUser = XOR<VIP, XOR<CommonUser, Visitor>>;
We can also use mutually exclusive types to implement binding effects, meaning either both properties A and B are present, or neither property is present:
type XORStruct = XOR<
{},
{
foo: string;
bar: number;
}
>;
Set Utility Types#
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
// Union
type Concurrence<A, B> = A | B;
// Intersection
type Intersection<A, B> = A extends B ? A : never;
// Difference
type Difference<A, B> = A extends B ? never : A;
// Complement
type Complement<A, B extends A> = Difference<A, B>;
type NonNullable<T> = T extends null | undefined ? never : T;
type _NonNullable<T> = Difference<T, null | undefined>
The specific implementation here is actually the distributed nature of conditional types, meaning when both T and U are union types (considered as a set), the members of T will be taken out one by one to perform the calculation extends U ? T1 : T2
, and then the final result will be merged back into a union type.
We can implement a version for object property names accordingly:
// Using more precise object type descriptions
type PlainObjectType = Record<string, any>;
// Union of property names
type ObjectKeysConcurrence<
T extends PlainObjectType,
U extends PlainObjectType
> = keyof T | keyof U;
// Intersection of property names
type ObjectKeysIntersection<
T extends PlainObjectType,
U extends PlainObjectType
> = Intersection<keyof T, keyof U>;
// Difference of property names
type ObjectKeysDifference<
T extends PlainObjectType,
U extends PlainObjectType
> = Difference<keyof T, keyof U>;
// Complement of property names
type ObjectKeysComplement<
T extends U,
U extends PlainObjectType
> = Complement<keyof T, keyof U>;
For intersection, complement, and difference, we can directly use the set of property names to implement the object-level version:
type ObjectIntersection<
T extends PlainObjectType,
U extends PlainObjectType
> = Pick<T, ObjectKeysIntersection<T, U>>;
type ObjectDifference<
T extends PlainObjectType,
U extends PlainObjectType
> = Pick<T, ObjectKeysDifference<T, U>>;
type ObjectComplement<T extends U, U extends PlainObjectType> = Pick<
T,
ObjectKeysComplement<T, U>
>;
type Merge<
T extends PlainObjectType,
U extends PlainObjectType
// The part of T that is more than U, plus the intersection of T and U (if types differ, U takes precedence, then add the parts that U has more than T)
> = ObjectDifference<T, U> & ObjectIntersection<U, T> & ObjectDifference<U, T>;
type Assign<
T extends PlainObjectType,
U extends PlainObjectType
// The part of T that is more than U, plus the intersection of T and U (if types differ, T takes precedence, then add the parts that U has more than T)
> = ObjectDifference<T, U> & ObjectIntersection<T, U> & ObjectDifference<U, T>;
type Override<
T extends PlainObjectType,
U extends PlainObjectType
// The part of T that is more than U, plus the intersection of T and U (if types differ, U takes precedence (reverse union))
> = ObjectDifference<T, U> & ObjectIntersection<U, T>;
Pattern Matching Utility Types#
type FunctionType = (...args: any) => any;
type Parameters<T extends FunctionType> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends FunctionType> = T extends (...args: any) => infer R ? R : any;
type FirstParameter<T extends FunctionType> = T extends (
arg: infer P,
...args: any
) => any
? P
: never;
type LastParameter<T extends FunctionType> = T extends (arg: infer P) => any
? P
: T extends (...args: infer R) => any
? R extends [...any, infer Q]
? Q
: never
: never;
type ClassType = abstract new (...args: any) => any;
interface ClassType<TInstanceType = any> {
new (...args: any[]): TInstanceType;
}
type ConstructorParameters<T extends ClassType> = T extends abstract new (
...args: infer P
) => any
? P
: never;
type InstanceType<T extends ClassType> = T extends abstract new (
...args: any
) => infer R
? R
: any;
TypeScript 4.7 supports the infer constraint feature to extract specific types.
type FirstArrayItemType<T extends any[]> = T extends [infer P extends string, ...any[]]
? P
: never;
We previously discussed a utility type for extracting the internal value type of a Promise, PromiseValue. There is also a built-in utility type in TypeScript that serves this purpose, and its implementation is more rigorous:
type Awaited<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F): any }
? F extends (value: infer V, ...args: any) => any
? Awaited<V>
: never
: T;
References:
https://juejin.cn/book/7086408430491172901