类的基本语法
本教程共 80 篇 · 第 30 篇 · 更新于 2026-08-10 · 约 12 分钟阅读
本节目标:学会用 class 定义类,掌握字段声明、构造函数和属性初始化规则,理解 strictPropertyInitialization 的作用。
前面我们用对象和接口组织数据,用函数封装行为。这种写法在小项目里挺顺手,但项目一大,“一堆散装的函数 + 一堆散装的数据”就会让人抓狂。
这时候就需要**类(class)**了。类可以把数据和操作数据的方法打包在一起,就像一个”模块”——但它不是技术上的模块,而是逻辑上的模块。TypeScript 在 JavaScript 的类基础上加了类型注解,让面向对象编程有了静态类型检查的能力。
一个最简单的类
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
}
const p = new Point(3, 5);
p.move(1, 2);
console.log(`${p.x}, ${p.y}`); // "4, 7"
这段代码里出现了三个关键部分,咱们逐个看。
字段声明
x: number 和 y: number 叫作字段声明(field declaration)。它们告诉 TypeScript 这个类有哪些属性,以及每个属性的类型。
class User {
name: string;
age: number;
email: string;
}
在 TypeScript 里,类属性必须先声明才能用。你不能像纯 JavaScript 那样直接在构造函数里 this.gender = "male" 就行——必须先声明 gender 字段,否则编译器不认。
Note这个规则源于 TypeScript 的类型系统是结构化的。编译器需要知道类的完整形状才能做类型检查。如果允许随意添加属性,类的形状就不确定了。
字段初始化器
声明字段时可以顺手给它一个默认值:
class Counter {
value = 0; // 类型自动推断为 number
label = "计数"; // 类型自动推断为 string
}
const c = new Counter();
console.log(c.value); // 0
console.log(c.label); // "计数"
初始化器的类型会用来推断字段类型——跟 const、let 一样。
构造函数
构造函数是类实例化时自动调用的方法。它负责把初始数据塞进新创建的对象里。
class Dog {
name: string;
breed: string;
constructor(name: string, breed: string) {
this.name = name;
this.breed = breed;
}
}
const d = new Dog("旺财", "金毛");
构造函数的写法和普通函数差不多:可以加参数类型注解、默认值,甚至重载。
class Point {
x: number;
y: number;
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
// 三种调用方式都合法
const p1 = new Point(); // (0, 0)
const p2 = new Point(5); // (5, 0)
const p3 = new Point(3, 7); // (3, 7)
构造函数有两个特殊的限制:
- 不能写返回类型注解——返回的一定是类实例,不用你操心。
- 不能有类型参数(泛型参数要放在类上,不能放在构造函数上)。
strictPropertyInitialization
TypeScript 7.0 默认开启 strict: true,而 strict 模式会自动开启 strictPropertyInitialization。这个选项强制所有非可选的类属性必须在声明时或构造函数里初始化。
看个例子:
class BadUser {
name: string; // ❌ 报错:Property 'name' has no initializer
age: number; // ❌ 报错:Property 'age' has no initializer
}
TypeScript 会提醒你:“name 没初始化,而且也没在构造函数里明确赋值。”
怎么修?三种方法:
方法一:声明时给默认值
class GoodUser {
name = "未设置";
age = 0;
}
方法二:在构造函数里赋值
class GoodUser {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
方法三:用 ! 断言告诉 TypeScript “我知道这个属性会被初始化,别管我了”
class GoodUser {
name!: string;
age!: number;
// 假设有个 init() 方法会在外部调用,负责给这两个属性赋值
init(name: string, age: number) {
this.name = name;
this.age = age;
}
}
Caution
!断言是”我相信这里不会有问题”,但 TypeScript 不再帮你检查。如果init()忘了调用,运行时name就是undefined。谨慎使用。
strictPropertyInitialization 还有一个重要行为:TypeScript 不会分析构造函数调用的其他方法内部来确定属性是否初始化。什么意思呢?
class BadUser {
name: string; // ❌ 仍然报错
constructor() {
this.setup();
}
setup() {
this.name = "default"; // 编译器不认为这是初始化
}
}
为什么?因为子类可能重写 setup() 方法,导致 name 实际上没有被初始化。TypeScript 选择最安全的策略:只看构造函数本体内的代码。
方法
类里的函数叫方法。写法和普通函数一样,可以加类型注解:
class Calculator {
value = 0;
add(n: number): this {
this.value += n;
return this;
}
subtract(n: number): this {
this.value -= n;
return this;
}
result(): number {
return this.value;
}
}
const calc = new Calculator();
console.log(calc.add(5).subtract(2).result()); // 3
Tip方法返回
this可以实现链式调用(method chaining),就像上面的calc.add(5).subtract(2).result()。后面会深入讲this类型。
在方法内部,访问类的属性和方法必须加 this.。 不加的话,TypeScript 会认为你在引用外层作用域的变量:
let x = 0;
class C {
x = 10;
m() {
x = 5; // ❌ 修改的是外层的 let x(类型不匹配会报错)
this.x = 5; // ✅ 修改的是类实例的 x 属性
}
}
Getter 和 Setter
类可以定义访问器(accessor)——也就是 getter 和 setter:
class Person {
private _age = 0;
get age(): number {
return this._age;
}
set age(value: number) {
if (value < 0) {
throw new Error("年龄不能为负数");
}
this._age = value;
}
}
const p = new Person();
p.age = 25; // 触发 setter 的校验
console.log(p.age); // 触发 getter,输出 25
TypeScript 对访问器有一些特殊规则:
- 如果只有
get没有set,属性自动变成readonly。 - 如果
set的参数类型没写,会从get的返回类型推断。
从 TypeScript 4.3 开始,getter 和 setter 可以有不同的类型——setter 的类型可以比 getter 更宽:
class Thing {
private _size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
const num = Number(value);
if (Number.isFinite(num)) {
this._size = num;
}
}
}
Note如果你只是要一个简单的公开属性,不需要在读写时做额外处理,那直接声明一个字段就好——没必要用 getter/setter。只有当你需要在读写时执行校验、转换等逻辑时,才需要访问器。
给 Java / C# 读者
如果你是从 Java 或 C# 转过来的,有几点需要适应:
类的类型是结构化的。 TypeScript 不按”类名”来判断类型兼容,而是按”形状”。两个没有任何继承关系的类,只要属性相同,就可以互相赋值:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
const p: Point1 = new Point2(); // ✅ 没问题!
这在 Java / C# 里是不可能的。习惯之后你会发现这其实很灵活,但刚开始可能会有点心理障碍。
没有”反射”。 TypeScript 的类型在编译后完全消失。你不能像 Java 那样 getClass() 或像 C# 那样 typeof(T).Name 来获取类型信息。typeof new Point() 永远返回 "object"。
小结
class定义类,包含字段声明、构造函数和方法。- 字段声明必须写类型注解,或者通过初始化器让 TypeScript 推断。
strictPropertyInitialization(strict 模式默认开启)强制属性在声明或构造函数里初始化。- 不想初始化但确定会赋值的情况,用
!断言——但要小心。 - 类方法内访问属性必须用
this.。 - getter/setter 用于需要校验或转换的读写场景;简单属性直接用字段就够了。
- TypeScript 的类类型按结构比较,不按类名。
现在你有了类的基本骨架。但 OOP 的一个核心理念是封装——怎么控制哪些属性对外可见、哪些藏在内部?下一章讲访问修饰符。