阿里云主机折上折
  • 微信号
Current Site:Index > Custom tool type translates this sentence into English, outputting only plain text without any additional content.

Custom tool type translates this sentence into English, outputting only plain text without any additional content.

Author:Chuan Chen 阅读数:34001人阅读 分类: TypeScript

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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.