TypeScript 实用技巧:从函数重载到映射类型与泛型

作者:代码拾荒

时间:2026-10-24

浏览:116

本文总结了 TypeScript 中容易被忽视的实用技巧,涵盖函数重载、映射类型(如 Partial、Pick)、类型断言、枚举标志位操作、元组及泛型与 infer 的高级用法。旨在帮助开发者摆脱 any,发挥类型系统的真正威力,提升代码安全性与开发效率。

文章目录

前言

在使用 TypeScript 的过程中,许多开发者虽然使用了它,却未能充分发挥其作用。之前的代码中常出现大量 any 类型,这不仅容易引发 Bug,也浪费了 TypeScript 类型系统的优势。本文整理了一些实用的 TypeScript 技巧,供后续开发参考。

下面直接通过代码示例进行说明。

函数重载

当需要实现不同参数组合的调用逻辑时,例如传入 user 参数时不传 flag,而传入 para 参数时需传 flag,可以通过函数重载来定义这些签名。


interface User {
  name: string;
  age: number;
}

const user = {
  name: 'Jack',
  age: 123
};

class SomeClass {

  public test(para: User): number;
  public test(para: number, flag: boolean): number;

  public test(para: User | number, flag?: boolean): number {
    // 具体实现
    return 1;
  }
}

const someClass = new SomeClass();

// ok
someClass.test(user);
someClass.test(123, false);

// Error
// someClass.test(123); 
//Argument of type 'number' is not assignable to parameter of type 'User'.
// someClass.test(user, false);
//Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'number'.

映射类型

在深入理解映射类型之前,需要先掌握 keyof、never、typeof 和 in 这几个基础概念。

keyof:用于获取接口(interface)的所有键名。


interface Point {
    x: number;
    y: number;
}

// type keys = "x" | "y"
type keys = keyof Point;

never:代表永远不会存在的值的类型。

官方描述如下:

the never type represents the type of values that never occur.


// 例子:进行编译时的全面的检查
type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

使用 never 可以确保在联合类型新增成员时,若未处理该情况会报错,从而编写出类型绝对安全的代码。

typeof:用于获取某个值的类型。


const a: number = 3

// 相当于: const b: number = 4
const b: typeof a = 4

in:用于检查对象上是否存在某个属性。


interface A {
  x: number;
}

interface B {
  y: string;
}

function doStuff(q: A | B) {
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}

映射类型的作用是将一个类型映射为另一个类型,简单来说,就是按照相同的形式转换旧类型的每个属性。

Partial, Readonly, Nullable, Required

  • Partial:将每个属性转换为可选属性。
  • Readonly:将每个属性转换为只读属性。
  • Nullable:将属性类型转换为原类型与 null 的联合类型。
  • Required:将每个属性转换为必选属性。

type Partial<T> = {
    [P in keyof T]?: T[P];
}

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

type Nullable<T> = { 
  [P in keyof T]: T[P] | null 
}

type Required<T> = {
  [P in keyof T]-?: T[P]
}

interface Person {
    name: string;
    age: number;
}

type PersonPartial = Partial<Person>;
type PersonReadonly = Readonly<Person>;
type PersonNullable = Nullable<Person>;

type PersonPartial = {
    name?: string | undefined;
    age?: number | undefined;
}

type PersonReadonly = {
    readonly name: string;
    readonly age: number;
}

type PersonNullable = {
      name: string | null;
      age: number | null;
}

interface Props {
  a?: number;
  b?: string;
}

const obj: Props = { a: 5 };

const obj2: Required<Props> = { a: 5 };
// Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

Pick, Record

  • Pick:选取一组属性以指定新类型。
  • Record:创建一组属性以指定新类型,常用于声明普通的 Object 对象。

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

type Record<K extends keyof any, T> = {
  [P in K]: T;
}

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

todo; // = const todo: TodoPreview


interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const nav: Record<Page, PageInfo> = {
  about: { title: "title1" },
  contact: { title: "title2" },
  home: { title: "title3" },
};

nav.about; // = const nav: Record

Exclude, Omit

  • Exclude:去除交集,返回剩余的部分。
  • Omit:适用于键值对对象的 Exclude,去除类型中包含的键值对。

type Exclude<T, U> = T extends U ? never : T
type Omit = Pick<T, Exclude<keyof T, K>>

// 相当于: type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "a",
  completed: false,
};

ReturnType

获取返回值类型,通常用于函数。


type ReturnType<T extends (...args: any) => any>
  = T extends (...args: any) => infer R ? R : any;

declare function f1(): { a: number; b: string };
type T1 = ReturnType<typeof f1>;
//    type T1 = {
//        a: number;
//        b: string;
//    }

此外还有更多映射类型,可查阅 Utility Types 参考文档。

类型断言

类型断言用于明确告知 TypeScript 值的详细类型,合理使用可以减少工作量。

例如,一个变量没有初始值,但已知其类型信息(可能来自后端返回)。除了设置初始值并使用 typeof 获取类型外,还可以使用类型断言来解决此类问题。


interface User { 
    name: string; 
    age: number; 
}

export default class someClass { 
    private user = {} as User;
} 

枚举

枚举类型分为数字类型和字符串类型,其中数字类型的枚举可用作标志位。


enum AnimalFlags {
    None = 0, 
    HasClaws = 1 << 0, 
    CanFly = 1 << 1, 
    HasClawsOrCanFly = HasClaws | CanFly 
}

interface Animal { 
    flags: AnimalFlags; 
   [key: string]: any; 
}

function printAnimalAbilities(animal: Animal) { 
    var animalFlags = animal.flags; 
    if (animalFlags & AnimalFlags.HasClaws) { 
        console.log('animal has claws'); 
    } 
    if (animalFlags & AnimalFlags.CanFly) { 
        console.log('animal can fly'); 
    } 
    if (animalFlags == AnimalFlags.None) { 
        console.log('nothing'); 
    } 
}

var animal = { flags: AnimalFlags.None }; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // animal has claws 
animal.flags &= ~AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly; 
printAnimalAbilities(animal); // animal has claws, animal can fly 

  • 使用 |= 来添加一个标志;
  • 组合使用 &= 和 ~ 来清理一个标志;
  • | 来合并标志。

这种用法虽不常见,但在 TypeScript 源码中可见类似代码。

前言

字符串类型的枚举可用于维护常量。


const enum TODO_STATUS {
  TODO = 'TODO',
  DONE = 'DONE',
  DOING = 'DOING'
}

function todos (status: TODO_STATUS): Todo[];

todos(TODO_STATUS.TODO)

元组

表示一个已知元素数量和类型的数组,各元素的类型不必相同。


let x: [string, number];
x = ['hello', 10]; 

在发出不固定数量的请求时,可以应用元组。


const requestList: any[] = [http.get<A>('http://some.1')]; // 设置为 any[] 类型 
if (flag) { 
    requestList[1] = (http.get<B>('http://some.2')); 
} 
const [ { data: a }, response ] = await Promise.all(requestList) as [Response<A>, Response<B>?]

范型

定义泛型后,有两种使用方式:一种是传入泛型类型,另一种是使用类型推断。


declare function fn<T>(arg: T): T; // 定义一个泛型函数 
const fn1 = fn<string>('hello'); // 第一种方式,传入泛型类型 
string const fn2 = fn(1); // 第二种方式,从参数 arg 传入的类型 number,来推断出泛型 T 的类型是 number 

以下是一个扁平数组结构建树形结构的例子。


// 转换前数据 
const arr = [ 
{ id: 1, parentId: 0, name: 'test1'}, 
{ id: 2, parentId: 1, name: 'test2'}, 
{ id: 3, parentId: 0, name: 'test3'} 
]; 
// 转化后 
[ { id: 1, parentId: 0, name: 'test1', 
    childrenList: [ { id: 2, parentId: 1, name: 'test2', childrenList: [] } ] }, 
    { id: 3, parentId: 0, name: 'test3', childrenList: [] } 
]


interface Item { 
    id: number; 
    parentId: number; 
    name: string; 
}

// 传入的 options 参数中,得到 childrenKey 的类型,然后再传给 TreeItem

interface Options<T extends string> { 
    childrenKey: T; 
} 
type TreeItem<T extends string> = Item & { [key in T]: TreeItem<T>[] | [] }; 
declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[]; 
listToTree(arr, { childrenKey: 'childrenList' }).forEach(i => i.childrenList) 

infer

表示在 extends 条件语句中待推断的类型变量。


type ParamType<T> = T extends (param: infer P) => any ? P : T; 

这句话的意思是:如果 T 能赋值给 (param: infer P) => any,则结果是 (param: infer P) => any 类型中的参数 P,否则返回为 T。


interface User { 
    name: string; 
    age: number; 
} 
type Func = (user: User) => void 
type Param = ParamType<Func>; // Param = User 
type AA = ParamType<string>; // string

示例如下。


// [string, number] -> string | number
type ElementOf<T> = T extends Array<infer E> ? E : never;

type TTuple = [string, number];

type ToUnion = ElementOf<TTuple>; // string | number


// T1 | T2 -> T1 & T2
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type Result = UnionToIntersection<T1 | T2>; // T1 & T2

总结

TypeScript 的类型限制功能非常强大。由于篇幅有限,联合类型、交叉类型等其他类型读者可自行查阅资料。刚开始接触泛型及其各种组合时会感到不熟练,建议在项目中逐步应用,争取将 Bug 降至最低限度。

到此这篇关于 TypeScript 实用小技巧的文章就介绍到这了,更多相关内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章