文章目录
- 一、为什么一定要学 TypeScript?
- 二、TypeScript 环境搭建
- 三、TypeScript 核心语法:给变量 “贴标签”—— 基础类型系统
- 四、TypeScript 面向对象:用 “类” 组织代码 —— 适合大型项目
- 五、TypeScript 泛型(Generic):写 “通用代码”
- 六、实战案例:用 TS 写一个完整的 TodoList(待办事项)
- 七、新手避坑指南:10 个最常见的 TS 错误及解决方案
- 1. 引入第三方库时提示 “找不到模块 ‘axios’”?
- 2. 函数参数可选,但使用时提示 “可能为 undefined”?
- 3. 类型 “string” 不能赋值给类型 “number”,但实际是数字字符串?
- 4. `typeof x === 'object'` 为什么会包含 `null`?
- 5. 泛型函数中无法访问属性 “name”?
- 6. 数组遍历后,元素类型还是 “unknown”?
- 7. 类型 “{}” 上不存在属性 “id”?
- 8. `enum` 枚举值是数字,想转成字符串报错?
- 9. 函数返回值类型不匹配,比如 “应该返回 number 却返回了 string”?
- 10. `tsc` 编译后,`dist` 文件夹没生成?
作为前端开发者,你是不是也遇到过这些坑?比如写了个函数传了字符串,却被当成数字用,运行时才报错;或者接手别人的项目,变量没注释,根本不知道该传什么值?TypeScript(简称 TS) 就是来解决这些问题的 —— 它给 JavaScript 加了 “类型保险”,让代码写得放心、改得省心,而且所有 JS 代码都能直接用,不用怕兼容问题。
一、为什么一定要学 TypeScript?
-
写代码时 “提前排雷”,不用等运行才报错
JavaScript 是 “运行时才翻脸” 的 —— 比如你给一个存年龄的变量赋值字符串
"18",写的时候不报错,用户用的时候才崩。而 TS 在你写代码时就会提醒:“这里要数字,不能传字符串!”举个例子:
// JS 里不报错,运行时才出问题 let age = 18 age = "18" // 写的时候没事,后续用 age 做计算就会NaN// TS 里直接报错,提前修改 let age: number = 18 age = "18" // 红色波浪线警告:“string 不能赋值给 number” -
IDE 帮你 “猜下一步”,写代码更快
有了类型信息,VS Code 能帮你自动补全代码、提示属性。比如你定义了一个用户对象
user: { name: string; age: number },输入user.时,IDE 会直接弹出name和age让你选,不用记属性名,也不用翻文档。 -
代码自带 “说明书”,团队协作不懵
不用写一堆注释,类型就是最好的文档。比如函数
function getUserInfo(id: number): User,一眼就知道 “要传数字 ID,返回用户对象”,新人接手也能快速明白逻辑。 -
完全兼容 JS,不用 “推倒重写”
你现有的 JS 项目,把文件后缀改成
.ts就能用,想什么时候加类型就什么时候加。比如先把核心函数加类型,其他代码慢慢改,没有迁移压力。
二、TypeScript 环境搭建
TS 不能直接运行,要先编译成 JS。
1. 快速试手:3 步跑通第一个 TS 程序
-
第一步:装 Node.js
TS 编译器需要 Node.js 支持,去 Node.js 官网 下 “LTS 版本”(长期支持版,更稳定),一路下一步安装。
装完打开命令行,输入
node -v和npm -v,能看到版本号就装好了(比如v18.17.0和9.6.7)。 -
第二步:装 TS 编译器
命令行输入:
npm install -g typescript装完输
tsc -v,看到Version 5.2.2这样的版本号,就说明编译器装好了。 -
第三步:写代码、编译、运行
-
新建一个文件夹(比如叫
ts-demo),里面新建文件hello.ts,写代码:// 定义一个函数,参数是字符串,返回也是字符串 function sayHello (name: string): string { return `Hello!我是 ${name},这是我的第一个 TS 程序` } // 调用函数并打印 console.log(sayHello("TypeScript")); -
编译:在命令行进入
ts-demo文件夹,输入tsc hello.ts,会自动生成hello.js(TS 代码编译后的 JS 文件,类型注解会被去掉)。 -
运行:输入
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. 常用基础类型:每个类型都带 “生活例子”
| 类型 | 例子 | 通俗解释(什么时候用) | 避坑提示 |
|---|---|---|---|
number | 18, -5, 3.14, 100n | 所有数字(年龄、价格、分数),100n 是超大数字(比如订单号) | 别用 new Number(18)(会生成对象,不是纯数字,容易出问题) |
string | "张三", 'hello', `年龄${age}` | 字符串(名字、地址、文本),` ` 是模板字符串,能插变量 | 模板字符串里插变量用 ${},比如 `我今年${age}岁` |
boolean | true, false | 布尔值(开关状态、是否选中) | 别写 let isDone: boolean = new Boolean(false)(同上,生成对象) |
| 字面量类型 | '男', '女', 1, 2 | 限定变量只能取某个值(比如性别只能是 “男” 或 “女”) | 常和(联合类型)一起用,比如 type Gender = ’ 男 ’ | ’ 女 ’ |
any | let x: any = 1; x = "a"; | 任意类型(关闭 TS 检查,相当于写 JS) | 尽量少用!用多了就失去 TS 的意义,替代方案是 unknown |
unknown | let x: unknown = 1; | 不知道类型(但比 any 安全,用之前要确认类型) | 不能直接用,比如 x.toUpperCase() 会报错,要先判断类型 |
void | function fn(): void {} | 无返回值(函数不返回东西,或只写 return;) | 主要用在函数返回值,变量用 void 只能赋值 undefined |
array | [1,2,3], ["a","b"] | 数组(列表数据,比如学生列表、商品列表) | 两种写法:number[](数字数组)或 Array<number>(效果一样) |
tuple | ['张三', 18] | 固定长度和类型的数组(比如 “名字 + 年龄” 的组合,不能多也不能少) | 越界访问会警告,比如 let user: [string, number] = ['张三', 18]; user[2] = 20 会报错 |
enum | enum 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.parse 和 JSON.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.json 的 rootDir 配置错误,找不到源码文件。
解决:确认 rootDir 是源码所在目录(比如 src),且 src 下有 .ts 文件,然后重新执行 tsc。

6181

被折叠的 条评论
为什么被折叠?



