Custom tool type translates this sentence into English, outputting only plain text without any additional content.
Basic Concepts of Utility Types
Utility types in TypeScript are practical tools built on top of the type system, allowing developers to create new types by combining and transforming existing types. These utility types are essentially generic types that take one or more type parameters and return a new type. The core value of utility types lies in their ability to provide abstraction at the type level, giving the type system stronger expressive power.
// Simplest example of utility types
type Nullable<T> = T | null;
type StringOrNumber = string | number;
// Using utility types
const maybeString: Nullable<string> = "hello" || null;
const value: StringOrNumber = 123;
Built-in Utility Types Explained
TypeScript provides a rich set of built-in utility types that cover common type transformation needs. The Partial
utility type makes all properties of a type optional:
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = Partial<User>;
// Equivalent to { id?: number; name?: string; age?: number; }
The Required
utility type does the opposite of Partial
, making all properties mandatory:
type RequiredUser = Required<PartialUser>;
// Equivalent to the original User interface
The Readonly
utility type makes all properties read-only:
type ReadonlyUser = Readonly<User>;
// All properties become readonly
Conditional Types and Inference
Conditional types are the foundation for creating complex utility types, allowing type selection based on conditions. The basic syntax resembles a ternary expression:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // false
The infer
keyword can extract type information within conditional types:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnpackPromise<Promise<string>>; // string
type B = UnpackPromise<number>; // number
Advanced Mapped Types
Mapped types allow the creation of new types based on existing ones by iterating over their properties:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
Modifiers can be added to change property characteristics:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type ReadonlyPoint = { readonly x: number; readonly y: number };
type Point = Mutable<ReadonlyPoint>; // { x: number; y: number; }
Type Predicates and Template Literal Types
Type predicate utilities can create type guard functions:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const test = (val: unknown) => {
if (isString(val)) {
// Here, val is inferred as string type
console.log(val.toUpperCase());
}
};
Template literal types can create string-based patterns:
type EventName<T extends string> = `${T}Changed`;
type Concat<A extends string, B extends string> = `${A}-${B}`;
type T0 = EventName<'foo'>; // 'fooChanged'
type T1 = Concat<'a', 'b'>; // 'a-b'
Practical Utility Type Implementations
Implement a DeepPartial
utility type that recursively makes all properties optional:
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
interface Nested {
a: {
b: number;
c: {
d: boolean;
};
};
}
type PartialNested = DeepPartial<Nested>;
/*
{
a?: {
b?: number;
c?: {
d?: boolean;
};
};
}
*/
Implement a type-safe Object.entries
:
type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
function objectEntries<T extends object>(obj: T): Entries<T> {
return Object.entries(obj) as Entries<T>;
}
const obj = { a: 1, b: '2' };
const entries = objectEntries(obj); // ['a', 1] | ['b', '2']
Type Composition and Advanced Patterns
Type composition enables more complex utility types:
type ValueOf<T> = T[keyof T];
type NonFunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type PickNonFunction<T> = Pick<T, NonFunctionKeys<T>>;
interface MixedProps {
id: number;
name: string;
update: () => void;
}
type DataProps = PickNonFunction<MixedProps>; // { id: number; name: string; }
Implement a recursive Required
utility type:
type DeepRequired<T> = T extends object ? {
[P in keyof T]-?: DeepRequired<T[P]>;
} : T;
type PartialUser = {
id?: number;
info?: {
name?: string;
age?: number;
};
};
type RequiredUser = DeepRequired<PartialUser>;
/*
{
id: number;
info: {
name: string;
age: number;
};
}
*/
Type-Safe API Design
Utility types can be used to create type-safe API interfaces:
type ApiResponse<T> = {
data: T;
error: null;
} | {
data: null;
error: string;
};
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
try {
const response = await fetch(url);
const data = await response.json();
return { data, error: null };
} catch (err) {
return { data: null, error: err.message };
}
}
// Provides clear type hints when used
const result = await fetchData<{ id: number }>('/api/user');
if (result.error) {
console.error(result.error);
} else {
console.log(result.data.id);
}
Type Gymnastics Practice
Implement a utility type that converts a union type to a tuple:
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type LastOf<T> =
UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never;
type Push<T extends any[], V> = [...T, V];
type UnionToTuple<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
true extends N ? [] : Push<UnionToTuple<Exclude<T, L>>, L>;
type Test = UnionToTuple<'a' | 'b' | 'c'>; // ['a', 'b', 'c']
Implement a recursive key path type:
type PathImpl<T, Key extends keyof T> =
Key extends string
? T[Key] extends Record<string, any>
? `${Key}.${PathImpl<T[Key], keyof T[Key]>}`
: Key
: never;
type Path<T> = PathImpl<T, keyof T>;
interface ComplexObject {
a: {
b: {
c: number;
d: string;
};
e: boolean;
};
f: number[];
}
type ComplexPaths = Path<ComplexObject>;
// "a.b.c" | "a.b.d" | "a.e" | "f"
Exploring the Boundaries of the Type System
While TypeScript's type system is powerful, it has its limits. Recursion depth is a common issue:
// This type will error when the depth is too large
type DeepArray<T> = T | DeepArray<T>[];
// Practical solution: Set a recursion depth limit
type DeepArray<T, Depth extends number> = {
done: T;
recur: T | DeepArray<T, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9][Depth]>;
}[Depth extends 0 ? 'done' : 'recur'];
type Test = DeepArray<string, 5>; // Maximum depth of 5
Handling circular reference types:
interface TreeNode {
value: number;
left?: TreeNode;
right?: TreeNode;
}
// Using a utility type to handle potentially circular references in Partial
type CyclicPartial<T> = T extends object ? {
[K in keyof T]?: CyclicPartial<T[K]>;
} : T;
type PartialTreeNode = CyclicPartial<TreeNode>;
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:内置工具类型(Partial, Required等)
下一篇:条件类型应用