TypeScript 实用技巧:从函数重载到映射类型与泛型
本文总结了 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 实用小技巧的文章就介绍到这了,更多相关内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
JavaScript函数定义的三种方式:声明、表达式与箭头函数
2026-11-05 13:50:55
JavaScript中escape、encodeURI和encodeURIComponent对空格的处理差异
2026-11-05 12:47:23








