告别“AnyScript”,用 TS 写出更健壮的代码

前言:为什么我们需要 TS?

如果你是一名前端开发者,大概率经历过这样的场景:

// 一段看似正常的 JS 代码
function greet(user) {
    return "Hello, " + user.name; 
}

greet(null); // 💥 程序炸了:Cannot read property 'name' of null

这种在运行时才暴露错误的问题,在 JavaScript 中太常见了。JS 最初的定位是简单的浏览器脚本语言,代码量很小。但如今,JS 已经可以全栈开发,代码量呈指数级增长,当年“出生简陋”的 JS,在面对大型应用时,就显得力不从心了。

TypeScript 的出现,就是为了解决 JavaScript 的“痛点”:

  • 不清不楚的数据类型:你永远不知道一个变量到底是个啥。
  • 逻辑上的漏洞:比如上面那个例子,漏掉了空值判断。
  • 访问不存在的属性:对象里根本没这个 key,你却偏要访问。
  • 低级的拼写错误:把 onClick 写成 onClcik,找半天 bug。

TS 的核心价值在于:它提供了一套静态类型检查系统,在代码运行之前(编译阶段)就帮你发现这些潜在的错误。

说白了,TS 就是 JS 的“盔甲”,穿上它,你写的代码会更健壮,重构起来也更有底气。


第一章:快速上手 TS 环境

在开始写 TS 之前,我们需要先搭建好环境。

1.1 安装 TypeScript

TypeScript 最终会被编译成 JavaScript,所以我们需要一个编译器。

# 全局安装 TypeScript
npm i typescript -g

# 检查是否安装成功
tsc -v 

1.2 编译与配置

安装完成后,我们就可以使用 tsc 命令来编译 .ts 文件了。

# 编译一个 ts 文件
tsc index.ts

# 初始化 tsconfig.json 配置文件,控制 tsc 的行为
tsc --init

# 监听文件变化,实时编译
tsc --watch

小技巧:  默认情况下,即使 TS 代码里有类型错误,tsc 也会强行编译成 JS 文件。如果你希望在有类型错误时阻止编译,可以在 tsconfig.json 中开启 noEmitOnError 选项:

{
    "compilerOptions": {
        "noEmitOnError": true
    }
}

第二章:TS 的核心——类型系统

类型系统是 TS 的精髓所在,掌握它,你就掌握了 TS 的 80%。

2.1 基础类型与类型声明

TS 支持与 JS 几乎相同的数据类型,并额外增加了一些新类型。

// 基本类型声明
let a: string; 
let b: number;
let c: boolean;

// 字面量类型:值本身即是类型,不能更改
let hello: 'hello'; 
hello = 'world'; // ❌ 报错:不能将类型'"world"'分配给类型'"hello"'

// 类型推论:TS 能根据赋值自动推导类型
let d = 99; // TS 自动推导 d 为 number 类型
// d = 'hello'; // ❌ 报错:类型不匹配

2.2 any vs unknown vs never

这三个类型是 TS 新增的,也是初学者最容易混淆的地方。

  • any:类型系统的“黑洞”
    它可以赋值给任意类型,也可以被任意类型赋值。不推荐使用,因为它会完全失去类型检查,让 TS 变成了“AnyScript”。

  • unknown:类型安全的“any”
    它可以存储任意类型的值,但不能直接赋值给其他类型,需要先进行类型断言或类型守卫。

    let unknownVar: unknown;
    let strVar: string;
    
    // strVar = unknownVar; // ❌ 报错:unknown 类型不能赋值给 string
    strVar = unknownVar as string; // ✅ 使用类型断言
    strVar = <string>unknownVar;   // ✅ 另一种断言写法
    
  • never:表示“永远不存在”的值
    通常用于函数抛出异常或死循环。

    function throwError(msg: string): never {
        throw new Error(msg);
    }
    // 几乎用不到,因为它更多是 TS 内部使用,比如做完整性检查。
    

金句:  any 是魔鬼,unknown 是保镖。能用 unknown 就别用 any

2.3 void 与函数返回值

void 表示函数没有返回值。但有一个小细节需要注意:

type VoidFunc = () => void;

let f1: VoidFunc = function() {
    return 666; // ✅ 允许!但返回值会被忽略
};

let f2: VoidFunc = () => 123; // ✅ 也允许!箭头函数简写隐式返回

// 虽然可以返回其他值,但不建议依赖这个返回值
const result = f1(); // result 类型是 void,而不是 number

为什么 TS 不强制 void 函数必须返回 undefined  这是为了兼容 JS 的习惯,比如数组的 forEach 中,你经常写 () => arr.push(1),这其实隐式返回了一个数字,但没人会去用这个返回值。

2.4 对象、数组与函数类型声明

  • 对象类型

    // 固定属性
    let person: { name: string, age?: number }; // ? 表示可选属性
    
    // 索引签名:属性名不确定
    let dict: { [key: string]: any }; 
    dict = { name: 'lgl', gender: 'male' };
    
    // 注意:object 和 Object 的区别
    let a: object; // 只能存非原始类型(对象、数组等)
    let b: Object; // 可以调用 Object 方法的类型,不能存 null、undefined
    // 实际开发中这两个很少直接用,更推荐上面那种字面量或接口方式
    
  • 数组类型

    let arr1: number[] = [1, 2, 3];
    let arr2: Array<number> = [1, 2, 3]; // 泛型写法
    
  • 函数类型

    // 声明一个函数类型
    let count: (x: number, y: number) => number;
    count = (x, y) => x + y;
    

2.5 元组(Tuple)与枚举(Enum)

  • 元组:固定长度、固定类型的数组。

    let tuple: [string, number] = ['hello', 123];
    // tuple = [123, 'hello']; // ❌ 报错:顺序不匹配
    
  • 枚举:一组命名常量,增强代码可读性。

    enum Color {
        Red,   // 默认 0
        Green, // 默认 1
        Blue   // 默认 2
    }
    console.log(Color.Red); // 0
    
    // 如果不想生成多余的 JS 代码,可以使用常量枚举(编译后直接内联替换)
    const enum Gender {
        Male = '男',
        Female = '女'
    }
    let myGender = Gender.Male; // 编译后直接变成 '男'
    

2.6 type 与接口(Interface)的灵活用法

  • 联合类型与交叉类型

    // 联合类型:或
    type Status = number | string;
    type Gender = '男' | '女';
    
    // 交叉类型:且
    type Person = { name: string } & { age: number };
    let p: Person = { name: 'lgl', age: 18 };
    
  • 类型别名 vs 接口

    • type 可以声明联合类型、交叉类型等,更灵活。
    • interface 更适合声明对象的形状,且可以重复声明合并。
    interface User {
        name: string;
    }
    interface User {
        age: number;
    }
    // User 最终类型为 { name: string; age: number; }
    

第三章:面向对象——类(Class)与抽象类

TS 对 ES6 的类进行了增强,提供了完整的面向对象支持。

3.1 类的构造器简写

传统的写法比较冗余:

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

一键简写:  在构造器参数前加上访问修饰符(publicprivateprotected),TS 会自动帮我们定义并赋值。

class Person {
    constructor(public name: string, public age: number) {}
}
const p = new Person('lgl', 18);
console.log(p.name); // 直接访问

3.2 抽象类(Abstract Class)

抽象类是用来被继承的,它不能实例化,可以包含具体实现,也可以包含抽象方法(只有定义,没有实现)。

abstract class Animal {
    abstract makeSound(): void; // 抽象方法,子类必须实现
    
    move(): void {
        console.log('roaming the earth...');
    }
}

class Dog extends Animal {
    makeSound(): void {
        console.log('汪汪汪!');
    }
}

// const animal = new Animal(); // ❌ 报错:无法创建抽象类的实例

抽象类 vs 接口:

特性 抽象类 接口
是否包含实现代码 ✅ 可以 ❌ 不可以
继承/实现数量 只能继承一个 可以实现多个
适用场景 提供通用基类,复用代码 定义契约,描述结构

第四章:进阶——泛型(Generics)

泛型允许你在定义函数、类、接口时,不预先指定具体类型,而是在使用时再确定。这是一个极其强大的工具。

// 一个“万能”的 identity 函数
function identity<T>(arg: T): T {
    return arg;
}

// 使用时显式指定类型
let output1 = identity<string>("myString");

// 或利用类型推论,自动推导
let output2 = identity(123); // output2 是 number 类型

泛型也可以用在类和接口中:

// 泛型类
class Stack<T> {
    private items: T[] = [];
    push(item: T) { this.items.push(item); }
    pop(): T | undefined { return this.items.pop(); }
}

const numStack = new Stack<number>();
numStack.push(1);
// numStack.push('a'); // ❌ 报错

// 泛型接口
interface KeyValue<K, V> {
    key: K;
    value: V;
}
const kv: KeyValue<string, number> = { key: 'age', value: 18 };

金句:  泛型让代码从“一劳永逸”变成“一劳多逸”,一次定义,多种类型复用。


第五章:装饰器(Decorator)——TS 的黑魔法

装饰器是 TS 在 2.0 版本引入的一个重要特性,它是一个特殊的函数,可以“装饰”类、方法、属性等。注意:装饰器是一项实验性特性,在 tsconfig.json 中需要开启 experimentalDecorators 选项。

记住一个口诀:类装饰器管构造,方法装饰器管行为,访问器管 get/set,属性管定义,参数管顺序。

5.1 类装饰器(Class Decorator)

应用场景:修改类的构造函数、添加静态属性、替换类本身。
入参:构造函数的构造函数(即 target: Function)。

// 场景:给类打上版本号标签
function addVersion(version: string) {
    return function <T extends { new(...args: any[]): {} }>(constructor: T) {
        return class extends constructor {
            version = version;
            // 还可以添加静态属性
            static apiUrl = '/api/v2';
        };
    };
}

@addVersion('1.0.0')
class UserApi {
    constructor(public name: string) {}
}

const user = new UserApi('lgl');
console.log((user as any).version); // '1.0.0'
console.log((UserApi as any).apiUrl); // '/api/v2'

5.2 方法装饰器(Method Decorator)

应用场景:日志记录、性能监控(@timer)、权限校验、修改返回值。
入参(target, propertyKey, descriptor)

// 场景:权限校验装饰器
function checkRole(role: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            // 模拟从上下文中获取当前用户
            const currentUser = { name: 'lgl', role: 'admin' }; 
            if (currentUser.role === role) {
                return originalMethod.apply(this, args);
            } else {
                throw new Error(`权限不足,需要 ${role} 角色`);
            }
        };
        return descriptor;
    };
}

class OrderService {
    @checkRole('admin')
    deleteOrder(id: number) {
        console.log(`订单 ${id} 已删除`);
    }
}

const service = new OrderService();
service.deleteOrder(1001); // ✅ 正常执行

5.3 访问器装饰器(Accessor Decorator)

应用场景:拦截 Getter/Setter 的读写操作,比如数据劫持、防抖。
入参:与方法装饰器完全相同 (target, propertyKey, descriptor)

// 场景:阻止属性被频繁修改(防抖 setter)
function debounceSet(wait: number) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalSet = descriptor.set!;
        let timer: NodeJS.Timeout | null = null;
        descriptor.set = function (value: any) {
            if (timer) clearTimeout(timer);
            timer = setTimeout(() => {
                originalSet.call(this, value);
                timer = null;
            }, wait);
        };
        return descriptor;
    };
}

class SearchBox {
    private _keyword: string = '';

    get keyword() { return this._keyword; }

    @debounceSet(300)
    set keyword(val: string) {
        this._keyword = val;
        console.log(`搜索关键词更新为: ${val}`); // 只会延迟 300ms 后输出最后一次
    }
}

const box = new SearchBox();
box.keyword = 'a';
box.keyword = 'ab';
box.keyword = 'abc'; // 只有这次会在 300ms 后触发打印

5.4 属性装饰器(Property Decorator)

应用场景:为属性定义元数据、映射数据库字段、依赖注入(DI)。
入参(target, propertyKey) —— 没有第三个参数!  且无法直接修改属性的值,通常配合 reflect-metadata 库使用。

// 场景:标记字段对应的数据库列名
function Column(name: string) {
    return function (target: any, propertyKey: string) {
        // 在原型上存储元数据
        if (!target.constructor.__columns) {
            target.constructor.__columns = {};
        }
        target.constructor.__columns[propertyKey] = name;
    };
}

class UserEntity {
    @Column('user_name')
    name: string;

    @Column('user_age')
    age: number;
}

// 模拟 ORM 读取映射关系
console.log((UserEntity as any).__columns); 
// { name: 'user_name', age: 'user_age' }

5.5 参数装饰器(Parameter Decorator)

应用场景:在 NestJS / Java Spring 中极为常见,用于获取路由参数 @Param('id')、注入服务等。
入参(target, methodKey, parameterIndex)

// 场景:标记参数为需要自动注入
function InjectUser(target: any, methodKey: string, parameterIndex: number) {
    if (!target.__injectParams) {
        target.__injectParams = {};
    }
    if (!target.__injectParams[methodKey]) {
        target.__injectParams[methodKey] = [];
    }
    target.__injectParams[methodKey].push(parameterIndex);
}

class OrderController {
    getOrders(@InjectUser userId: number) {
        console.log(`查询用户 ${userId} 的订单`);
    }
}

// 模拟框架在调用时发现 __injectParams,自动从 session 中取出 userId 填进去

5.6 装饰器执行顺序小贴士

当多个装饰器作用于同一个目标时,执行顺序为:

属性/方法/参数装饰器 先执行(从下到上) → 类装饰器 最后执行

// 执行顺序:参数装饰器 -> 方法装饰器 -> 类装饰器
@ClassDecorator
class Example {
    @MethodDecorator
    method(@ParamDecorator param: any) {}
}

第六章:TypeScript 高级类型编程(解锁“类型体操”)

只会写 interface 和 type 只是入门。真正的中级进阶,是学会操作类型,把类型当成“值”来处理。

6.1 工具类型(Utility Types)——TS 官方自带的“万能工具箱”

TypeScript 内置了几个超级好用的工具类型,直接帮我们省去大量重复定义。

工具类型 作用 实战场景
Partial<T> 将 T 中所有属性变为可选 更新数据时,只传部分字段
Required<T> 将 T 中所有属性变为必选 强制补全配置项
Pick<T, K> 从 T 中挑选几个属性组成新类型 只取接口返回的某几个字段
Omit<T, K> 从 T 中剔除几个属性组成新类型 排除敏感字段(如密码)
Record<K, T> 构造一个对象类型,键为 K,值为 T 做字典映射
Readonly<T> 将 T 中所有属性变为只读 防止对象被意外修改
interface User {
    id: number;
    name: string;
    password: string;
    email: string;
}

// 场景1:更新时,只用传 id 和想改的字段
type UpdateUserParams = Partial<Omit<User, 'id'>> & { id: number };

// 场景2:前端展示列表,去掉敏感 password
type PublicUser = Omit<User, 'password'>;

// 场景3:做个国家编码映射
type CountryMap = Record<string, string>; 
const map: CountryMap = { CN: '中国', US: '美国' };

金句:  Pick 和 Omit 是你处理庞大后端接口返回数据时的最佳战友,能极大增强前端的类型安全性。

6.2 映射类型(Mapped Types)——定制自己的工具类型

明白了 Partial 的原理,你就能创造自己的工具类型。

// 手写一个 Partial(原理揭秘)
type MyPartial<T> = {
    [P in keyof T]?: T[P];
};

// 手写一个 Readonly(只读)
type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 进阶:给所有属性加上前缀(比如转成 getter 风格)
type Getters<T> = {
    [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
// 如果 T 有 name: string,会生成 getName: () => string

6.3 条件类型(Conditional Types)——类型的“if/else”

条件类型让类型具备逻辑判断能力,语法是 T extends U ? X : Y

// 场景:根据泛型自动推断返回的数组类型
type ElementType<T> = T extends any[] ? T[number] : T;

type A = ElementType<string[]>; // A 为 string
type B = ElementType<number>;   // B 为 number

// 高级结合:过滤掉函数类型
type NonFunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

interface Mixed {
    name: string;
    age: number;
    eat: () => void;
}
type OnlyProps = NonFunctionPropertyNames<Mixed>; // 'name' | 'age'

6.4 infer 关键字——类型推断的“占位符”

infer 是条件类型里的“占位符”,用于提取类型的某一部分。

// 提取函数返回值类型(手写 ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
    return { id: 1, name: 'lgl' };
}
type UserType = MyReturnType<typeof getUser>; // { id: number; name: string; }

// 提取数组元素类型
type MyArrayElement<T> = T extends (infer U)[] ? U : never;
type Str = MyArrayElement<string[]>; // string

6.5 类型守卫(Type Guards)——让 TS 在代码块内“变聪明”

有时候 TS 无法自动推断联合类型的具体分支,需要我们用 is 关键字来收窄类型。

// 自定义类型守卫
function isString(value: any): value is string {
    return typeof value === 'string';
}

function process(input: string | number) {
    if (isString(input)) {
        // 在这个块里,TS 知道 input 一定是 string
        console.log(input.toUpperCase()); 
    } else {
        console.log(input.toFixed(2));
    }
}

// 另一种方式:typeof 类型守卫
function process2(input: string | number) {
    if (typeof input === 'string') {
        console.log(input.length); // TS 自动收窄为 string
    }
}

6.6 内置高级类型补充

工具类型 作用
Parameters<T> 获取函数 T 的参数类型(元组形式)
ReturnType<T> 获取函数 T 的返回值类型
InstanceType<T> 获取构造函数 T 的实例类型
NonNullable<T> 从 T 中剔除 null 和 undefined
function greet(name: string, age: number): string {
    return `${name} is ${age} years old`;
}

type GreetParams = Parameters<typeof greet>; // [string, number]
type GreetReturn = ReturnType<typeof greet>; // string

第七章:实战最佳实践总结

7.1 开发建议

  1. 始终开启 strict 模式:在 tsconfig.json 中开启严格模式,它会帮你检查更多潜在问题。
  2. 避免使用 any:把它当作最后的手段,优先考虑 unknown 或更具体的类型。
  3. 善用类型推导:能自动推导出来的类型,就不用显式声明,保持代码简洁。
  4. 使用 interface 定义对象形状,使用 type 定义联合类型
  5. 泛型是你的好帮手:写可复用的组件、函数时,优先考虑泛型。
  6. 装饰器谨慎使用:虽然强大,但过度使用会降低代码可读性,适合框架层使用。

7.2 tsconfig.json 推荐配置(快速上手版)

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "noEmitOnError": true,
        "outDir": "./dist",
        "rootDir": "./src"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "dist"]
}

写在最后(高阶心法)

  1. 类型编程不是炫技:在团队协作中,可读性永远大于“骚操作”。如果写一个复杂的泛型能让调用方爽到,那就写;如果只是自己爽,别人看吐血,那请加上详细注释。
  2. 理解 keyof 和 inkeyof 取键的联合类型,in 遍历联合类型。这是映射类型的基石。
  3. TS 版本迭代很快:关注 TypeScript 官方 Release Notes,每个版本都会带来新工具类型和语法糖,保持学习。

TypeScript 就像是 JavaScript 的“静态显微镜”,语法是皮,类型思维是骨。当你习惯了用类型去“约束”和“推导”时,你就会发现,代码写完即完工,Bug 无处遁形。

 

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐