Combining generics with conditional types
Combining Generics with Conditional Types
The combination of generics and conditional types in TypeScript greatly enhances the flexibility of the type system. Generics enable code reuse, while conditional types dynamically determine output types based on input types. When combined, the type system can handle more complex scenarios, such as type filtering, type mapping, or type inference.
Review of Generics Basics
Generics are a crucial tool in TypeScript for creating reusable components. They allow defining functions, interfaces, or classes without specifying concrete types in advance, deferring the type specification until usage.
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("hello");
Generic constraints can limit the range of generic parameter types:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
Basics of Conditional Types
Conditional types select different type branches based on type relationships, with the syntax T extends U ? X : Y
.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Conditional types are often used for type operations:
type NonNullable<T> = T extends null | undefined ? never : T;
Combining Generics with Conditional Types
Combining generics with conditional types allows for the creation of more flexible type utilities. A classic example is the Extract
and Exclude
utility types:
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
This combination enables filtering specific types from union types based on conditions:
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Exclude<"a" | "b" | "c", "a" | "f">; // "b" | "c"
Distributive Conditional Types
When conditional types operate on union types, they trigger distributive behavior, meaning the condition is applied separately to each member of the union:
type ToArray<T> = T extends any ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]
To avoid distributive behavior, wrap the types in square brackets:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Arr = ToArrayNonDist<string | number>; // (string | number)[]
Type Inference in Conditional Types
In the extends
clause of a conditional type, the infer
keyword can declare type variables to extract types:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function foo() { return 123; }
type FooReturn = ReturnType<typeof foo>; // number
This method can also extract function parameter types:
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function bar(x: string, y: number) {}
type BarParams = Parameters<typeof bar>; // [string, number]
Recursive Conditional Types
Conditional types can recursively reference themselves to handle nested structures:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
const obj = {
a: 1,
b: {
c: 2,
d: {
e: 3
}
}
};
type ReadonlyObj = DeepReadonly<typeof obj>;
/*
{
readonly a: number;
readonly b: {
readonly c: number;
readonly d: {
readonly e: number;
};
};
}
*/
Template Literal Types with Conditional Types
Combining template literal types enables more powerful type operations:
type GetterName<T extends string> = `get${Capitalize<T>}`;
type NameGetters = GetterName<'name' | 'age'>; // "getName" | "getAge"
Further combining with conditional types allows for complex transformations:
type MethodNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type Methods = MethodNames<{
id: number;
getName(): string;
getAge(): number;
}>; // "getName" | "getAge"
Type Constraints in Conditional Types
In conditional types, constraints can be added to infer
-deduced types:
type FirstIfString<T> =
T extends [infer S, ...unknown[]]
? S extends string
? S
: never
: never;
type A = FirstIfString<["hello", 42]>; // "hello"
type B = FirstIfString<[42, "hello"]>; // never
Practical Type Examples
Combining generics and conditional types allows for the creation of many utility type tools:
type Flatten<T> = T extends Array<infer U> ? U : T;
type Nested = Array<Array<number>>;
type Flat = Flatten<Nested>; // Array<number>
type PromiseType<T> = T extends Promise<infer U> ? U : T;
type Promised = Promise<string>;
type Unwrapped = PromiseType<Promised>; // string
Conditional Types and Function Overloads
Conditional types can simplify function overload declarations:
function process<T extends string | number>(
input: T
): T extends string ? string : number {
return typeof input === "string" ? input.toUpperCase() : input.toFixed(2);
}
const str = process("hello"); // string
const num = process(3.14159); // number
Type Predicates and Conditional Types
Combining type predicates enables more precise type guards:
type IsArray<T> = T extends any[] ? true : false;
function assertArray<T>(value: T): asserts value is IsArray<T> extends true ? T : never {
if (!Array.isArray(value)) {
throw new Error("Not an array");
}
}
const unknownValue: unknown = [1, 2, 3];
assertArray(unknownValue);
// Here, unknownValue is inferred as any[]
Conditional Types and Mapped Types
Combining mapped types enables more complex type transformations:
type PartialByKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = PartialByKeys<User, 'name' | 'age'>;
/*
{
id: number;
name?: string;
age?: number;
}
*/
Conditional Types and Indexed Access
Conditional types can be combined with indexed access types for dynamic type queries:
type PropType<T, K> =
K extends keyof T ? T[K] :
K extends `${infer F}.${infer R}` ?
F extends keyof T ?
PropType<T[F], R> :
never :
never;
interface User {
name: string;
address: {
street: string;
city: string;
};
}
type StreetType = PropType<User, 'address.street'>; // string
Conditional Types and never
The never
type has special behavior in conditional types and is often used for type exclusion:
type ExcludeNever<T> = T extends never ? never : T;
type WithoutNever = ExcludeNever<string | number | never>; // string | number
Conditional Types and Function Types
Conditional types can distinguish between different function types:
type AsyncFunction<T> = T extends (...args: any[]) => Promise<any> ? T : never;
function isAsync<T>(fn: T): fn is AsyncFunction<T> {
return fn instanceof Function && fn.constructor.name === "AsyncFunction";
}
async function asyncFn() {}
function syncFn() {}
const test1 = isAsync(asyncFn); // true
const test2 = isAsync(syncFn); // false
Conditional Types and Classes
Conditional types can also be applied to class type checks:
type Constructor<T> = new (...args: any[]) => T;
class Animal {}
class Dog extends Animal {}
type IsAnimal<T> = T extends Animal ? true : false;
type Test1 = IsAnimal<Dog>; // true
type Test2 = IsAnimal<string>; // false
Conditional Types and Variadic Tuples
Combining with variadic tuple types enables more flexible type operations:
type Join<T extends unknown[], D extends string> =
T extends [] ? '' :
T extends [infer F] ? F :
T extends [infer F, ...infer R] ? `${F & string}${D}${Join<R, D>}` :
string;
type Path = Join<['user', 'profile', 'name'], '/'>; // "user/profile/name"
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn