Generic in Type Aliases#
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface IFoo {
prop1: string;
prop2: number;
prop3: boolean;
prop4: () => void;
}
type PartialIFoo = Partial<IFoo>;
// Equivalent to
interface PartialIFoo {
prop1?: string;
prop2?: number;
prop3?: boolean;
prop4?: () => void;
}
In the combination of type aliases and generics, besides mapped types, index types, and other type utilities, there is another important tool: conditional types.
type IsEqual<T> = T extends true ? 1 : 2;
type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'linbudu'>; // 2
Generic Constraints and Default Values#
type Factory<T = boolean> = T | number | string;
In addition to declaring default values, generics can also do something that function parameters cannot do: generic constraints. This means that you can require the generic type parameter of this utility type to meet certain conditions, otherwise you will refuse to proceed with the subsequent logic. In generics, we can use the extends
keyword to constrain the generic parameter passed in to meet the requirements. Regarding extends
, A extends B
means that A is a subtype of B, here we only need to understand very simple judgment logic, that is, A is more accurate or more complex than the type of B. Specifically, it can be divided into the following categories.
- More accurate, such as literal types are subtypes of corresponding primitive types, that is,
'linbudu' extends string
,599 extends number
holds. Similarly, union types whose subsets are all subtypes of union types, that is,1
and1 | 2
are subtypes of1 | 2 | 3 | 4
. - More complex, such as
{ name: string }
is a subtype of{}
, because it adds additional types based on{}
, and the same applies to base classes and derived classes.
type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"
type Res3 = ResStatus<'10000'>; // Type 'string' does not satisfy the constraint 'number'.
In TypeScript, generic parameters have default constraints (also in function generics and Class generics below). The default constraint value was any
before version 3.9 of TS, and unknown
since version 3.9. In TypeScript ESLint, you can use the no-unnecessary-type-constraint rule to avoid declaring generic constraints that are the same as the default constraint in your code.
Multiple Generic Associations#
type Conditional<Type, Condition, TruthyResult, FalsyResult> =
Type extends Condition ? TruthyResult : FalsyResult;
// "passed!"
type Result1 = Conditional<'linbudu', string, 'passed!', 'rejected!'>;
// "rejected!"
type Result2 = Conditional<'linbudu', boolean, 'passed!', 'rejected!'>;
Multiple generic parameters are actually like functions that accept more parameters, and the internal logic (type operations) will be more abstract. This is manifested in the logical operations (type operations) that the parameters (generic parameters) need to perform will be more complex.
As mentioned earlier, the dependency between multiple generic parameters actually means that in the subsequent generic parameters, the previous generic parameters are used as constraints or default values:
type ProcessInput<
Input,
SecondInput extends Input = Input,
ThirdInput extends Input = SecondInput
> = number;
Generic in Object Types#
interface IRes<TData = unknown> {
code: number;
error?: string;
data: TData;
}
interface IUserProfileRes {
name: string;
homepage: string;
avatar: string;
}
function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}
type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}
interface IPaginationRes<TItem = unknown> {
data: TItem[];
page: number;
totalCount: number;
hasNextPage: boolean;
}
function fetchUserProfileList(): Promise<IRes<IPaginationRes<IUserProfileRes>>> {}
Generic in Functions#
Suppose we have a function that can accept multiple types of parameters and process them accordingly, such as:
- For strings, return a partial substring;
- For numbers, return its n times;
- For objects, modify its properties and return.
function handle<T>(input: T): T {}
const handle = <T>(input: T): T => {}
const handle = <T extends any>(input: T): T => {}
const author = "linbudu"; // Declared using const, inferred as "linbudu"
let authorAge = 18; // Declared using let, inferred as number
handle(author); // Filled with literal type "linbudu"
handle(authorAge); // Filled with basic type number
We have declared a generic parameter T for the function and assigned the parameter type and return value type to this generic parameter. This way, when this function receives a parameter, T will be automatically filled with the type of this parameter. This also means that you no longer need to determine the possible types of parameters in advance, and in the case of the return value being associated with the parameter type, you can also perform calculations using generic parameters.
When filling generics based on parameter types, the type information will be inferred to the most accurate degree, such as inferring to literal types rather than basic types here. This is because when a value is passed directly, it will not be modified again, so it can be inferred to the most accurate degree. If you use a variable as a parameter, only the type marked by this variable will be used (if not marked, the inferred type will be used).
function swap<T, U>([start, end]: [T, U]): [U, T] {
return [end, start];
}
const swapped1 = swap(["linbudu", 599]);
const swapped2 = swap([null, 599]);
const swapped3 = swap([{ name: "linbudu" }, {}]);
Generic parameters of functions will also be consumed by internal logic, such as:
function handle<T>(payload: T): Promise<[T]> {
return new Promise<[T]>((res, rej) => {
res([payload]);
});
}
Generic in Classes#
class Queue<TElementType> {
private _list: TElementType[];
constructor(initial: TElementType[]) {
this._list = initial;
}
// Enqueue an element of a subtype of the queue generic type
enqueue<TType extends TElementType>(ele: TType): TElementType[] {
this._list.push(ele);
return this._list;
}
// Enqueue an element of any type (no need to be a subtype of the queue generic type)
enqueueWithUnknownType<TType>(element: TType): (TElementType | TType)[] {
return [...this._list, element];
}
// Dequeue
dequeue(): TElementType[] {
this._list.shift();
return this._list;
}
}
Generic in Built-in Methods#
function p() {
return new Promise<boolean>((resolve, reject) => {
resolve(true);
});
}
const arr: Array<number> = [1, 2, 3];
// Type 'string' cannot be assigned to type 'number'.
arr.push('linbudu');
// Type 'string' cannot be assigned to type 'number'.
arr.includes('linbudu');
// number | undefined
arr.find(() => false);
// First reduce
arr.reduce((prev, curr, idx, arr) => {
return prev;
}, 1);
// Second reduce
// Error: Type 'number' is not assignable to type 'never'.
arr.reduce((prev, curr, idx, arr) => {
return [...prev, curr]
}, []);
The reduce method is a relatively special one, and its type declaration has several different overloads:
- When you do not pass an initial value, the generic parameter will be filled from the element type of the array.
- When you pass an initial value, if the type of the initial value is the same as the element type of the array, the element type of the array will be used for filling. That is, the first reduce call here.
- When you pass an array type as the initial value, such as the second reduce call here, the generic parameter of reduce will be filled with the type inferred from this initial value by default, such as
never[]
.
The third case means that there is insufficient information to infer the correct type, and we can manually pass in the generic parameter to solve it:
arr.reduce<number[]>((prev, curr, idx, arr) => {
return prev;
}, []);
References:
https://juejin.cn/book/7086408430491172901