TypeScript

文章目录


作为前端开发者,你是不是也遇到过这些坑?比如写了个函数传了字符串,却被当成数字用,运行时才报错;或者接手别人的项目,变量没注释,根本不知道该传什么值?TypeScript(简称 TS) 就是来解决这些问题的 —— 它给 JavaScript 加了 “类型保险”,让代码写得放心、改得省心,而且所有 JS 代码都能直接用,不用怕兼容问题。

一、为什么一定要学 TypeScript?

  1. 写代码时 “提前排雷”,不用等运行才报错

    JavaScript 是 “运行时才翻脸” 的 —— 比如你给一个存年龄的变量赋值字符串 "18",写的时候不报错,用户用的时候才崩。而 TS 在你写代码时就会提醒:“这里要数字,不能传字符串!”

    举个例子:

    // JS 里不报错,运行时才出问题
    let age = 18
    age = "18" // 写的时候没事,后续用 age 做计算就会NaN
    
    // TS 里直接报错,提前修改
    let age: number = 18
    age = "18" // 红色波浪线警告:“string 不能赋值给 number”
    
  2. IDE 帮你 “猜下一步”,写代码更快

    有了类型信息,VS Code 能帮你自动补全代码、提示属性。比如你定义了一个用户对象 user: { name: string; age: number },输入 user. 时,IDE 会直接弹出 nameage 让你选,不用记属性名,也不用翻文档。

  3. 代码自带 “说明书”,团队协作不懵

    不用写一堆注释,类型就是最好的文档。比如函数 function getUserInfo(id: number): User,一眼就知道 “要传数字 ID,返回用户对象”,新人接手也能快速明白逻辑。

  4. 完全兼容 JS,不用 “推倒重写”

    你现有的 JS 项目,把文件后缀改成 .ts 就能用,想什么时候加类型就什么时候加。比如先把核心函数加类型,其他代码慢慢改,没有迁移压力。

二、TypeScript 环境搭建

TS 不能直接运行,要先编译成 JS。

1. 快速试手:3 步跑通第一个 TS 程序

  • 第一步:装 Node.js

    TS 编译器需要 Node.js 支持,去 Node.js 官网 下 “LTS 版本”(长期支持版,更稳定),一路下一步安装。

    装完打开命令行,输入 node -vnpm -v,能看到版本号就装好了(比如 v18.17.09.6.7)。

  • 第二步:装 TS 编译器

    命令行输入:

    npm install -g typescript
    

    装完输 tsc -v,看到 Version 5.2.2 这样的版本号,就说明编译器装好了。

  • 第三步:写代码、编译、运行

  1. 新建一个文件夹(比如叫 ts-demo),里面新建文件 hello.ts,写代码:

    // 定义一个函数,参数是字符串,返回也是字符串
    function sayHello (name: string): string {
      return `Hello!我是 ${name},这是我的第一个 TS 程序`
    }
    
    // 调用函数并打印
    console.log(sayHello("TypeScript"));
    
  2. 编译:在命令行进入 ts-demo 文件夹,输入 tsc hello.ts,会自动生成 hello.js(TS 代码编译后的 JS 文件,类型注解会被去掉)。

  3. 运行:输入 node hello.js,就能看到输出 Hello!我是 TypeScript,这是我的第一个 TS 程序

2. 实际项目:用 tsconfig.json 统一配置

如果要写正经项目,不能每次都手动编译单个文件,需要用 tsconfig.json 控制整个项目的编译规则(比如编译后的 JS 放哪里、兼容哪个 JS 版本)。

第一步:生成配置文件

在项目根目录(比如 my-ts-project)打开命令行,输入:

tsc --init

会自动生成 tsconfig.json 文件,里面有很多注释,我们只需要改几个核心配置。

第二步:改核心配置(新手必改 5 项)

打开 tsconfig.json,找到 compilerOptions 里的这些配置,改成下面这样:

{
   "compilerOptions": {
      "target": "ES6", // 编译后的 JS 版本(ES6 兼容性好,大部分浏览器都支持)
      "module": "ESNext", // 模块规范(跟前端框架配合用 ESNext 就行)
      "outDir": "./dist", // 编译后的 JS 文件放 dist 文件夹(方便管理)
      "rootDir": "./src", // 你的 TS 源码放 src 文件夹(规范目录结构)
      "strict": true,  // 开启严格模式(必开!强制检查类型,避免隐性 Bug)

      // 下面这几个默认就行,不用改
      "esModuleInterop": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true
   },
   "include": ["./src/**/*"], // 要编译的文件:src 下所有子文件夹的 .ts 文件
   "exclude": ["node_modules"] // 不用编译的文件:依赖包(装的第三方库)
}

简单说:你把 TS 代码写在 src 里,编译后会自动生成到 dist 里,项目运行时用 dist 里的 JS 文件。

第三步:用配置文件编译

在项目根目录输入 tsc(不用加文件名),TS 会自动按照 tsconfig.json 的规则,把 src 里所有 TS 文件编译成 JS 放到 dist 里。

三、TypeScript 核心语法:给变量 “贴标签”—— 基础类型系统

TS 的核心就是 “给变量定类型”,比如 “这个变量是数字”“那个函数返回字符串”。下面讲最常用的类型,每个都带 “实际用法” 和 “避坑提示”。

1. 先搞懂:类型声明 vs 自动推断

新手不用每次都手动写类型,TS 会 “猜” 类型,这两种方式都能用:

  • 手动声明:明确告诉 TS 变量类型,语法 let 变量名: 类型 = 值;

  • 自动推断:TS 根据你给的初始值,自动猜类型(推荐优先用这个,少写冗余代码)

举个例子:

// 手动声明:告诉 TS age 是数字
let age: number = 18;

// 自动推断:TS 看到初始值是 18,就知道 age2 是数字
let age2 = 18;

// 两种方式效果一样,后面改值时都不能传非数字
age = 20; // 对

// age = "20"; // 错:string 不能赋值给 number

2. 常用基础类型:每个类型都带 “生活例子”

类型例子通俗解释(什么时候用)避坑提示
number18, -5, 3.14, 100n所有数字(年龄、价格、分数),100n 是超大数字(比如订单号)别用 new Number(18)(会生成对象,不是纯数字,容易出问题)
string"张三", 'hello', `年龄${age}`字符串(名字、地址、文本),` ` 是模板字符串,能插变量模板字符串里插变量用 ${},比如 `我今年${age}岁`
booleantrue, false布尔值(开关状态、是否选中)别写 let isDone: boolean = new Boolean(false)(同上,生成对象)
字面量类型'男', '女', 1, 2限定变量只能取某个值(比如性别只能是 “男” 或 “女”)常和(联合类型)一起用,比如 type Gender = ’ 男 ’ | ’ 女 ’
anylet x: any = 1; x = "a";任意类型(关闭 TS 检查,相当于写 JS)尽量少用!用多了就失去 TS 的意义,替代方案是 unknown
unknownlet x: unknown = 1;不知道类型(但比 any 安全,用之前要确认类型)不能直接用,比如 x.toUpperCase() 会报错,要先判断类型
voidfunction fn(): void {}无返回值(函数不返回东西,或只写 return;主要用在函数返回值,变量用 void 只能赋值 undefined
array[1,2,3], ["a","b"]数组(列表数据,比如学生列表、商品列表)两种写法:number[](数字数组)或 Array<number>(效果一样)
tuple['张三', 18]固定长度和类型的数组(比如 “名字 + 年龄” 的组合,不能多也不能少)越界访问会警告,比如 let user: [string, number] = ['张三', 18]; user[2] = 20 会报错
enumenum Status { 待支付, 已支付 }命名常量(比如订单状态、按钮状态,不用记数字或字符串)推荐用字符串枚举,比如 enum Status { 待支付 = 'PENDING', 已支付 = 'PAID' },语义更清晰

3. 重点类型深讲:新手最容易懵的 3 个类型

(1)any vs unknown:为什么说 any 是 “毒苹果”?

any 就像 “不管不顾”,写的时候爽,但运行时容易崩;unknown 是 “小心谨慎”,用之前要确认身份。

举个实际场景:后端返回的数据,不知道是什么类型。

// 用 any:危险!
let res: any = await axios.get("/api/user");

// 不管 res.data 是什么类型,都能写 res.data.name,但如果后端返回的是数字,运行时就报错
console.log(res.data.name);

// 用 unknown:安全!
let res: unknown = await axios.get("/api/user");

// 先判断类型,再用
if (typeof res === "object" && res !== null && "data" in res) {
 const user = res.data as { name: string }; // 确认是用户对象
 console.log(user.name); // 安全
}

简单说:不确定类型时,用 unknown 而不是 any,多写一行判断,少一个线上 Bug。

(2)tuple 元组:固定结构的数组

元组适合存 “结构固定” 的数据,比如地图上的坐标(x 轴 + y 轴)、键值对(key + value)。

例子:用元组存坐标和用户信息

// 坐标:第一个是 x 轴(数字),第二个是 y 轴(数字)
type Point = [number, number]
const pos: Point = [100, 200] // 正确
// const pos: Point = [100, "200"]; // 错误:第二个必须是数字

// 用户信息:名字(字符串)+ 年龄(数字)+ 是否成年(布尔)
type UserInfo = [string, number, boolean]
const user: UserInfo = ["张三", 18, true] // 正确
// const user: UserInfo = ["张三", 18]; // 错误:少一个布尔值

(3)enum 枚举:不用记 “魔法值”,代码更易读

“魔法值” 就是没注释的数字或字符串,比如 if (order.status === 1),别人根本不知道 1 代表什么。用枚举就能解决这个问题。

例子:订单状态管理

// 定义枚举:给每个状态起名字
enum OrderStatus {
   待支付 = "PENDING",
   已支付 = "PAID",
   已发货 = "SHIPPED",
   已完成 = "COMPLETED"
}

// 使用:不用记字符串,直接用枚举名
const order = {
   id: 1,
   status: OrderStatus.待支付 // 语义清晰
}

// 判断状态:不用写 "PENDING",不容易写错
if (order.status === OrderStatus.待支付) {
   console.log("请尽快支付")
}

简单说:枚举就是给 “固定值” 起个好记的名字,让代码不用猜。

4. 类型断言:告诉 TS“我比你懂,听我的”

有时候你知道变量的类型,但 TS 不知道,这时候就用 “类型断言”—— 相当于 “拍胸脯保证”:“这个变量就是这个类型,出问题我负责”。

语法有两种,推荐用第一种(和 JSX 兼容):

  • 值 as 类型(推荐)

  • <类型>值(不推荐,和 React 等框架的标签冲突)

实际场景:获取 DOM 元素(TS 不知道你选的是 div 还是 span)

// 1. 获取 id 为 "app" 的元素,TS 只知道是 HTMLElement(通用元素类型)
const app = document.getElementById("app");

// 2. 你知道这是 div 元素,用断言告诉 TS
const appDiv = app as HTMLDivElement;

// 3. 现在可以安全访问 div 特有的属性(比如 style)
appDiv.style.color = "red";

// 避坑:如果 app 是 null(没找到元素),会报错,所以最好加判断
if (app) {
   const appDiv = app as HTMLDivElement;
   appDiv.style.color = "red";
}

四、TypeScript 面向对象:用 “类” 组织代码 —— 适合大型项目

面向对象就是 “把相关的属性和方法打包成一个类”,比如 “人” 有名字、年龄(属性),会说话、走路(方法),用 class 就能定义一个 “人类”。

1. 类(Class):创建对象的 “模板”

比如定义一个 “人类”,用这个模板创建 “张三”“李四” 等具体的人。

// 定义“人类”模板
class Person {
  // 简化写法:构造函数参数加 public,自动声明并赋值属性(不用单独写 name: string; age: number;)
  constructor(public name: string, public age: number) { }

  // 方法:人会说话
  sayHello (): void {
    console.log(`大家好,我是 ${this.name},今年 ${this.age}`)
  }

  // 静态方法:属于“人类”这个模板,不是某个具体的人(不用创建实例就能用)
  static isAdult (age: number): boolean {
    return age >= 18 // 判断是否成年
  }
}

// 用模板创建“张三”这个具体的人(实例化)
const zhangsan = new Person("张三", 20)

zhangsan.sayHello() // 输出:大家好,我是 张三,今年 20 岁

// 调用静态方法:直接用类名调用,不用创建实例
console.log(Person.isAdult(20)) // 输出:true(20岁成年)

简单说class 是模板,new Class() 是具体的对象,静态方法是 “模板的工具”,不用造对象就能用。

2. 封装:给属性 “加锁”,不让随便改

比如 “人” 的年龄,不能随便改(比如改成 200 岁),这时候就把年龄设为 “私有”,只能通过指定方法修改。

class Person {
  // 私有属性:加 private,只能在类内部访问,外部不能直接改
  private _age: number

  constructor(public name: string, age: number) {
    this._age = age // 初始化年龄,只能在类内部赋值
  }

  // getter:获取年龄(对外提供“读”的权限)
  get age (): number {
    return this._age
  }

  // setter:修改年龄(对外提供“写”的权限,还能加验证)
  set age (newAge: number) {
    if (newAge < 0 || newAge > 150) { // 年龄不能小于0或大于150
      throw new Error("年龄必须在 0-150 之间!")
    }
    this._age = newAge
  }
}

// 使用
const lisi = new Person("李四", 25)
console.log(lisi.age) // 输出:25(调用 getter)
lisi.age = 30 // 正确(调用 setter,30 在 0-150 之间)
// lisi.age = 200; // 报错:年龄必须在 0-150 之间!
// lisi._age = 30; // 报错:_age 是 private,外部不能访问

实际用途:比如用户的余额、积分等敏感数据,不能让外部随便改,只能通过 “充值”“消费” 等方法修改,就用这种方式。

3. 继承:少写重复代码

比如 “动物” 有名字、会吃东西,“狗” 和 “猫” 继承 “动物”,不用重复写名字和吃的方法,只需要加自己的特色(狗会叫,猫会喵喵叫)。

// 父类:动物(通用属性和方法)
class Animal {
  constructor(public name: string) { }

  // 动物都会吃东西
  eat (): void {
    console.log(`${this.name} 在吃东西`)
  }
}

// 子类:狗(继承动物,加自己的方法)
class Dog extends Animal {
  // 子类构造函数必须调用 super(),把名字传给父类
  constructor(name: string, public breed: string) { // breed:品种
    super(name) // 调用父类的构造函数,传名字
  }

  // 重写父类方法:狗吃东西有自己的特点
  eat (): void {
    super.eat() // 先调用父类的 eat 方法
    console.log(`${this.name}${this.breed})喜欢吃骨头`)
  }

  // 子类新增方法:狗会汪汪叫
  bark (): void {
    console.log(`${this.name} 在汪汪叫`)
  }
}

// 用 Dog 类创建“旺财”
const wangcai = new Dog("旺财", "金毛")
wangcai.eat() // 输出:旺财 在吃东西 → 旺财(金毛)喜欢吃骨头
wangcai.bark() // 输出:旺财 在汪汪叫

简单说:继承就是 “拿别人的代码当基础,再加自己的东西”,减少重复代码。

4. 抽象类:不能直接用的 “模板的模板”

比如 “形状” 是抽象的,你不能说 “我画了一个形状”,只能说 “我画了一个圆形”“我画了一个正方形”。抽象类就是这样 —— 只能被继承,不能直接创建实例。

// 抽象类:形状(用 abstract 修饰)
abstract class Shape {
  constructor(public name: string) { }

  // 抽象方法:只有声明,没有实现(子类必须自己写实现)
  abstract calculateArea (): number // 计算面积

  // 普通方法:有实现,子类可以直接用
  printName (): void {
    console.log(`这是一个 ${this.name}`)
  }
}

// 子类:圆形(继承形状,必须实现 calculateArea)
class Circle extends Shape {
  constructor(name: string, public radius: number) { // radius:半径
    super(name)
  }

  // 实现抽象方法:计算圆形面积(πr²)
  calculateArea (): number {
    return Math.PI * this.radius * this.radius
  }
}

// 子类:正方形(继承形状,必须实现 calculateArea)
class Square extends Shape {
  constructor(name: string, public sideLength: number) { // sideLength:边长
    super(name)
  }

  // 实现抽象方法:计算正方形面积(边长²)
  calculateArea (): number {
    return this.sideLength * this.sideLength
  }
}

// 使用
const circle = new Circle("圆形", 5)
circle.printName() // 输出:这是一个 圆形
console.log(circle.calculateArea()) // 输出:78.539...(π*5²)
// const shape = new Shape("形状"); // 报错:抽象类不能直接创建实例

实际用途:定义 “通用规范”,比如所有组件都要有的 render 方法,就可以用抽象类约束。

5. 接口(Interface):定义 “结构契约”

接口和抽象类有点像,但更灵活 —— 它只定义 “有什么属性和方法”,不管怎么实现,还能让多个类遵守同一个规则。

场景 1:约束对象结构

// 定义接口:用户必须有 id(数字)和 name(字符串),age 可选
interface User {
  id: number
  name: string
  age?: number // 可选属性,可写可不写
  readonly email: string // 只读属性,初始化后不能改
}

// 用接口约束变量:必须符合 User 的结构
const user1: User = {
  id: 1,
  name: "张三",
  email: "zhangsan@example.com"
}

const user2: User = {
  id: 2,
  name: "李四",
  age: 20, // 可选属性,写了也没问题
  email: "lisi@example.com"
}

// user2.email = "new@example.com"; // 报错:email 是只读属性

场景 2:约束类的结构

// 定义接口:会飞的东西必须有 fly 方法
interface Flyable {
  fly (): void
}

// 定义接口:会游泳的东西必须有 swim 方法
interface Swimmable {
  swim (): void
}

// 类:鸭子(既会飞又会游泳,实现两个接口)
class Duck implements Flyable, Swimmable {
  // 必须实现 Flyable 的 fly 方法
  fly (): void {
    console.log("鸭子扇着翅膀飞")
  }

  // 必须实现 Swimmable 的 swim 方法
  swim (): void {
    console.log("鸭子在水里游")
  }
}

// 使用
const duck = new Duck()
duck.fly() // 输出:鸭子扇着翅膀飞
duck.swim() // 输出:鸭子在水里游

简单说:接口就是 “规则清单”,对象或类必须按清单来,保证结构统一。

五、TypeScript 泛型(Generic):写 “通用代码”

泛型就是 “类型参数化”—— 比如写一个 “获取数组第一个元素” 的函数,既能处理数字数组,又能处理字符串数组,不用写两个函数。

1. 泛型函数:一个函数适配多种类型

比如 “获取数组第一个元素”,支持数字、字符串、对象等各种数组。

// 泛型函数:<T> 是类型参数,代表“某种类型”(T 可以随便起,比如 <Type>)
function getFirstElement<T> (arr: T[]): T | undefined {
  return arr[0] // 返回值类型和数组元素类型一致
}

// 场景 1:处理数字数组
const numbers = [1, 2, 3]
const firstNum = getFirstElement(numbers)
// TS 自动推断 T 是 number,firstNum 类型是 number | undefined

// 场景 2:处理字符串数组
const strings = ["a", "b", "c"]
const firstStr = getFirstElement(strings)
// TS 自动推断 T 是 string,firstStr 类型是 string | undefined

// 场景 3:处理对象数组(比如 User 数组)
interface User { id: number; name: string }
const users: User[] = [{ id: 1, name: "张三" }, { id: 2, name: "李四" }]
const firstUser = getFirstElement(users)
// TS 自动推断 T 是 User,firstUser 类型是 User | undefined

实际用途:工具函数(比如深拷贝、数组处理)都用泛型,比如 JSON.parseJSON.stringify 其实就是泛型函数。

2. 泛型类:一个类适配多种类型

比如 “本地存储工具类”,既能存用户信息,又能存商品列表,不用写多个存储类。

// 泛型类:<T> 是要存储的数据类型
class LocalStorage<T> {
  // 存数据:key 是字符串,value 是 T 类型
  setItem (key: string, value: T): void {
    localStorage.setItem(key, JSON.stringify(value))
  }

  // 取数据:返回 T 类型或 null
  getItem (key: string): T | null {
    const val = localStorage.getItem(key)
    return val ? JSON.parse(val) : null
  }
}

// 场景 1:存用户信息(T 是 User 类型)
interface User { id: number; name: string }
const userStorage = new LocalStorage<User>()
userStorage.setItem("user", { id: 1, name: "张三" }) // 只能存 User 类型
const user = userStorage.getItem("user") // 取出来还是 User 类型

// 场景 2:存商品列表(T 是 { id: number; name: string } 类型)
type Product = { id: number; name: string }
const productStorage = new LocalStorage<Product[]>()
productStorage.setItem("products", [{ id: 1, name: "手机" }, { id: 2, name: "电脑" }])
const products = productStorage.getItem("products") // 取出来是 Product[] 类型

3. 泛型约束:给泛型 “加限制”

有时候泛型不能太随意,比如写一个 “获取长度” 的函数,只能处理有 length 属性的类型(比如字符串、数组),不能处理数字。

// 1. 定义接口:有 length 属性的类型
interface HasLength {
  length: number
}

// 2. 泛型约束:T 必须是 HasLength 的子类型(必须有 length 属性)
function getLength<T extends HasLength> (item: T): number {
  return item.length
}

// 正确使用:字符串、数组都有 length 属性
console.log(getLength("hello")) // 5(字符串长度)
console.log(getLength([1, 2, 3])) // 3(数组长度)
// 错误使用:数字没有 length 属性
// console.log(getLength(123)); // 报错:number 没有 length 属性

简单说:泛型约束就是 “给通用代码加规则”,避免传错类型。

六、实战案例:用 TS 写一个完整的 TodoList(待办事项)

结合前面学的 “接口、类、泛型”,写一个能 “增删改查” 的 TodoList,覆盖实际开发中的核心逻辑。

1. 需求分析

需要实现的功能:

  • 添加待办事项

  • 切换待办状态(完成 / 未完成)

  • 编辑待办内容

  • 删除待办事项

  • 获取所有待办事项

2. 完整代码实现

// 1. 定义 Todo 类型(接口):每个待办有 id、内容、状态
interface Todo {
  id: number // 唯一 ID(用时间戳生成)
  content: string // 待办内容
  done: boolean // 完成状态:true 完成,false 未完成
  createTime: number // 创建时间(时间戳)
}

// 2. 实现 TodoList 类:封装所有功能
class TodoList {
  // 私有属性:存储待办列表,外部不能直接修改
  private todos: Todo[] = [];

  /**
   * 添加待办事项
   * @param content 待办内容
   */
  addTodo (content: string): void {
    if (!content.trim()) { // 内容不能为空
      throw new Error("待办内容不能为空!")
    }

    const newTodo: Todo = {
      id: Date.now(), // 用当前时间戳当唯一 ID
      content: content.trim(),
      done: false,
      createTime: Date.now()
    }

    this.todos.push(newTodo)
  }

  /**
   * 切换待办状态(完成 / 未完成)
   * @param id 待办 ID
   */
  toggleTodo (id: number): void {
    const todo = this.todos.find(t => t.id === id)

    if (!todo) { // 没找到对应 ID 的待办
      throw new Error(`未找到 ID 为 ${id} 的待办事项!`)
    }

    todo.done = !todo.done // 反转状态
  }

  /**
   * 编辑待办内容
   * @param id 待办 ID
   * @param newContent 新的待办内容
   */
  editTodo (id: number, newContent: string): void {
    if (!newContent.trim()) {
      throw new Error("待办内容不能为空!")
    }

    const todo = this.todos.find(t => t.id === id)

    if (!todo) {
      throw new Error(`未找到 ID 为 ${id} 的待办事项!`)
    }

    todo.content = newContent.trim()
  }

  /**
   * 删除待办事项
   * @param id 待办 ID
   */
  deleteTodo (id: number): void {
    const index = this.todos.findIndex(t => t.id === id)

    if (index === -1) {
      throw new Error(`未找到 ID 为 ${id} 的待办事项!`)
    }

    this.todos.splice(index, 1) // 从数组中删除
  }

  /**
   * 获取所有待办事项(返回副本,避免外部修改原数组)
   * @returns Todo[] 待办列表
   */
  getTodos (): Todo[] {
    return [...this.todos] // 用扩展运算符返回新数组
  }

  /**
   * 获取完成的待办事项
   * @returns Todo[] 完成的待办列表
   */
  getDoneTodos (): Todo[] {
    return this.todos.filter(t => t.done)
  }
}

// 3. 使用 TodoList
const myTodoList = new TodoList()

// 添加待办
myTodoList.addTodo("学习 TypeScript 基础")
myTodoList.addTodo("用 TS 写 TodoList")
myTodoList.addTodo("整理笔记")
console.log("初始待办列表:", myTodoList.getTodos())

/* 输出:
  [
    { id: 1698000000000, content: '学习 TypeScript 基础', done: false, createTime: 1698000000000 },
    { id: 1698000000001, content: '用 TS 写 TodoList', done: false, createTime: 1698000000001 },
    { id: 1698000000002, content: '整理笔记', done: false, createTime: 1698000000002 }
  ]
*/

// 切换第一个待办为完成
const firstTodoId = myTodoList.getTodos()[0].id
myTodoList.toggleTodo(firstTodoId)
console.log("完成的待办:", myTodoList.getDoneTodos()) // 输出第一个待办

// 编辑第二个待办
const secondTodoId = myTodoList.getTodos()[1].id
myTodoList.editTodo(secondTodoId, "用 TS 写一个完整的 TodoList")
console.log("编辑后的待办列表:", myTodoList.getTodos()[1].content) // 输出新内容

// 删除第三个待办
const thirdTodoId = myTodoList.getTodos()[2].id
myTodoList.deleteTodo(thirdTodoId)
console.log("删除后的待办数量:", myTodoList.getTodos().length) // 输出 2

七、新手避坑指南:10 个最常见的 TS 错误及解决方案

1. 引入第三方库时提示 “找不到模块 ‘axios’”?

原因:TS 不知道 axios 的类型(需要 .d.ts 类型文件)。

解决:安装 axios 的类型包(大部分库的类型包都在 @types 下):

npm install @types/axios --save-dev

2. 函数参数可选,但使用时提示 “可能为 undefined”?

例子

// 可选参数 name 可能是 undefined
function greet(name?: string) {
   console.log(`Hello ${name.toUpperCase()}`); // 报错:name 可能为 undefined
}

解决:加默认值或判断:

// 方案 1:加默认值
function greet (name: string = "Guest") {
  console.log(`Hello ${name.toUpperCase()}`)
}

// 方案 2:判断是否存在
function greet (name?: string) {
  if (name) { // 存在才执行
    console.log(`Hello ${name.toUpperCase()}`)
  } else {
    console.log("Hello Guest")
  }
}

3. 类型 “string” 不能赋值给类型 “number”,但实际是数字字符串?

例子

const ageStr = "18"; // 数字字符串
const age: number = ageStr; // 报错:string 不能赋值给 number

解决:用 Number()parseInt() 转换类型:

const age: number = Number(ageStr); // 正确,18 是数字
// 或
const age: number = parseInt(ageStr, 10); // 正确,10 是进制

4. typeof x === 'object' 为什么会包含 null

原因:JS 历史 Bug,typeof null === 'object',TS 继承了这个特性。

例子

function isObject(x: unknown): x is object {
   return typeof x === "object"; // 错误:x 为 null 时也会返回 true
}

解决:额外判断 x !== null

function isObject(x: unknown): x is object {
   return typeof x === "object" && x !== null;
}

5. 泛型函数中无法访问属性 “name”?

例子

// 想访问 obj.name,但 TS 不知道 obj 有没有 name
function printName<T>(obj: T) {
   console.log(obj.name); // 报错:T 上不存在属性 name
}

解决:用泛型约束,限制 T 必须有 name 属性:

interface HasName {
   name: string;
}

function printName<T extends HasName>(obj: T) {
   console.log(obj.name); // 正确,T 一定有 name
}

6. 数组遍历后,元素类型还是 “unknown”?

例子

const arr: unknown[] = [1, "2", 3]

arr.forEach(item => {
  console.log(item + 1) // 报错:item 是 unknown,不能直接运算
})

解决:遍历中判断类型:

arr.forEach(item => {
  if (typeof item === "number") {
    console.log(item + 1) // 正确,item 是数字
  } else if (typeof item === "string") {
    console.log(Number(item) + 1) // 正确,转成数字再运算
  }
})

7. 类型 “{}” 上不存在属性 “id”?

例子

const user = {}; // TS 推断为 {}(空对象)
user.id = 1; // 报错:{} 上不存在属性 id

解决:提前定义类型或用类型断言:

// 方案 1:提前定义类型
interface User { id?: number }
const user: User = {}
user.id = 1 // 正确

// 方案 2:类型断言
const user = {} as User
user.id = 1 // 正确

8. enum 枚举值是数字,想转成字符串报错?

例子

enum Status { 待支付, 已支付 } // 默认是数字枚举,Status.待支付 = 0
const statusStr: string = Status.待支付; // 报错:number 不能赋值给 string

解决:用字符串枚举,或用 String() 转换:

// 方案 1:字符串枚举(推荐)
enum Status {
  待支付 = "PENDING",
  已支付 = "PAID"
}
const statusStr: string = Status.待支付 // 正确,"PENDING" 是字符串

// 方案 2:转换数字枚举
const statusStr: string = String(Status.待支付) // 正确,"0" 是字符串

9. 函数返回值类型不匹配,比如 “应该返回 number 却返回了 string”?

例子

function add(a: number, b: number): number {
   return `${a + b}`; // 报错:返回的是 string,不是 number
}

解决:要么改返回值类型,要么改返回的内容:

// 方案 1:改返回值类型为 string
function add (a: number, b: number): string {
  return `${a + b}` // 正确
}

// 方案 2:改返回内容为 number
function add (a: number, b: number): number {
  return a + b // 正确
}

10. tsc 编译后,dist 文件夹没生成?

原因tsconfig.jsonrootDir 配置错误,找不到源码文件。

解决:确认 rootDir 是源码所在目录(比如 src),且 src 下有 .ts 文件,然后重新执行 tsc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值