news 2026/9/10 6:06:09

深入掌握 TypeScript 类(Class):类型标注、可见性、this 绑定与泛型实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入掌握 TypeScript 类(Class):类型标注、可见性、this 绑定与泛型实战指南

深入掌握 TypeScript 类(Class):类型标注、可见性、this 绑定与泛型实战指南

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

TypeScript 类是 ES6 JavaScript 类的类型化超集,在类成员(字段、构造器、方法、访问器)上叠加了类型标注、public/private/protected可见性修饰符、readonly字段、static成员与参数属性(parameter properties)等能力。本文以简单的User类为主线,系统讲解 TypeScript 类的类型化编写方式、this绑定的两种可靠方案、泛型类与多接口实现,并结合开源仓库 refine 中真实的KeyBuilder类链(源码位置),帮助读者在实战项目中写出类型安全、结构清晰的类。

环境准备

TypeScript 类语法与编译行为受tsconfig.json中的compilerOptions控制。本文示例的核心开关是strictPropertyInitialization(严格属性初始化检查),它控制未初始化字段是否报错:

// tsconfig.json { "compilerOptions": { "strictPropertyInitialization": true } }

在 TypeScript Playground 中,可以通过TS Config下拉菜单 →Type Checking分组 → 勾选strictPropertyInitialization来开启同一行为。本文部分示例(如对比类形状一节)会注明"在strictPropertyInitialization: false下运行",请按示例前的注释调整配置,以免被非主题的初始化错误干扰。

Typing Class Members in TypeScript

TypeScript 类通常为其成员(以及适用场景下的参数)添加类型标注。下面依次覆盖字段、构造器、方法、访问器及其参数的类型化细节。

字段类型标注

字段的类型标注方式与普通变量一致——有初始值时类型可被推断,无初始值时需显式声明。看一个带几个字段的User类:

class User { username = "randomString"; firstName: string; lastName: string; age!: number; }

username的类型由初始值"randomString"推断为stringfirstNamelastNameage!则显式声明为string/number。其中age后紧跟的!确定赋值断言运算符(definite assignment assertion)。

字段初始化选项

TypeScript 类语法为字段初始化提供了三种选择:在声明处初始化、保持未初始化、或未初始化但预期在运行时某个时刻被"确定地"赋值。Userusername属于第一种,姓名两个字段属于第二种,age!属于第三种。

确定赋值断言(Definite Field Assignments)

age未初始化,但伴随!运算符。它告诉 TypeScript:即使当前未初始化,也不会触发严格属性初始化报错,但该字段预期在稍后会被赋上指定类型的值。典型场景是字段由外部库的 API 填充:

const joe = new User(); // 从外部设置 joe 的年龄 joe.age = getUserInfoFromStatsBureau("someId")?.data?.age;

严格字段初始化(Strict Field Initialization)

开启--strictPropertyInitialization后,所有字段必须满足以下条件之一:有初始化器、在构造器中赋值、或使用确定赋值断言标记为稍后赋值。否则 TypeScript 抛出2564错误:

// 开启 --strictPropertyInitialization class User { // 已初始化,不报错 username = "randomString"; // 在构造器中赋值,不报错 private firstName: string; // 构造器中未赋值 private lastName: string; // Property 'lastName' has no initializer and is not definitely assigned in the constructor.(2564) // 去掉 bang (!) 同样报 2564 错误 protected age: number; // Property 'age' has no initializer and is not definitely assigned in the constructor.(2564) constructor(firstName: string) { this.firstName = firstName; } }

成员可见性(Access Modifiers)

TypeScript 为类成员提供publicprotectedprivate三种可见性,与 ES2022 中 JavaScript 的#私有字段语法是两套不同的机制。可见性适用于所有成员(字段、方法、访问器),此处以字段为例。

未指定任何修饰符的字段默认为public,可从实例访问与赋值:

class User { username = "randomString"; firstName: string; lastName: string; age!: number; } const joe = new User(); joe.username = "jos3ph"; joe.firstName = "Joseph"; joe.lastName = "Hiyden"; joe.age = 63; console.log(joe.username); // "jos3ph" console.log(`${joe.firstName} ${joe.lastName}`); // "Joseph Hiyden" console.log(joe.age); // 63

private将访问与赋值限制在类内部;protected额外允许子类访问。因此从实例上访问或设置private/protected字段会报错:

class User { username = "randomString"; private firstName: string; private lastName: string; protected age!: number; } const joe = new User(); joe.username = "jos3ph"; joe.firstName = "Joseph"; // Property 'firstName' is private and only accessible within class 'User'.(2341) joe.lastName = "Hidden"; // Property 'firstName' is private and only accessible within class 'User'.(2341) joe.age = 63; // Property 'age' is protected and only accessible within class 'User' and its subclasses.(2445) console.log(joe.username); console.log(`${joe.firstName} ${joe.lastName}`); // 2341 Errors console.log(joe.age); // 2445 Error

静态成员(Static Members)

与 JavaScript 一致,TypeScript 使用static关键字将成员挂在类本身而非实例上:

class User { public static userType: string = "Guest"; username = "randomString"; protected age!: number; } console.log(User.userType); // "Guest"

一个容易踩的坑:声明静态字段时,可见性修饰符必须放在static之前,否则 TypeScript 报错:

'public' modifier must precede 'static' modifier.(1029)

此外,从源码层面看,静态成员还有一个重要限制——不能引用类的类型参数(见下文"泛型类"一节)。

readonly字段

readonly字段只能被初始化为两类时机之一:声明处初始化,或构造器内赋值。即使定义了 setter,也不能在 setter 或实例上重新赋值:

class User { static userType: string = "Guest"; readonly _username: string = "randomString"; // 声明处初始化,不报错 protected age!: number; get username() { return this._username; } set username(username: string) { // setter 内重新赋值报错 this._username = username; // Cannot assign to '_username' because it is a read-only property.(2540) } constructor(username: string) { this._username = username; // 构造器内赋值不报错 } } const dona = new User("trump"); // 实例属性赋值报错(编译期拦截) console.log((dona._username = "trump_trippin")); // Cannot assign to '_username' because it is a read-only property.(2540)

构造函数(Constructor Functions)

构造器参数与普通 TypeScript 函数参数一样进行类型标注:

class User { username = "randomString"; private firstName: string; private lastName: string; protected age!: number; constructor(firstName: string, lastName: string) { this.firstName = firstName; this.lastName = lastName; } }

需要注意两个关键点:

  1. 构造器自身不能接受类型参数。不存在constructor<AbsurdTypeParam>() {}这种写法;类型参数属于类声明本身(用于泛型类,见后文):
class User { // 构造器函数不能接受类型参数 constructor<AbsurdTypeParam>() {} // 非法 }
  1. 构造器无需标注返回类型。构造器总是返回实例的类型,即类自身创建的那个类型:
class User { username = "randomString"; private firstName: string; private lastName: string; protected age!: number; // 构造器的返回类型就是类实例的类型 constructor(firstName: string, lastName: string) { // constructor User(firstName: string, lastName: string): User this.firstName = firstName; this.lastName = lastName; } } // joe 的类型是 User const joe = new User("Joe", "Hiyden"); // joe: User

类会创建自身的类型——joe的类型就是User,类定义既可作为值使用(new),也可作为类型使用(标注变量)。

参数属性(Parameter Properties)

参数属性允许把构造器参数直接转成类字段:只要给构造器参数加上可见性修饰符(public/private/protected)和/或readonly,就无需在类顶部声明字段,也无需在构造器体内手动赋值:

class User { username = "randomString"; protected age!: number; // 在构造器参数上使用字段修饰符声明参数属性 constructor(private firstName: string, private lastName: string) { // 构造器体内不再需要赋值 } fullName() { return `${this.firstName} ${this.lastName}`; } } const joe = new User("Joe", "Hiyden"); joe.firstName; // Property 'firstName' is private and only accessible within class 'User'.(2341) console.log(joe.fullName()); // "Joe Hiyden"

重构后的User类顶部不再声明firstName/lastName字段,构造器体内也不需要赋值语句,代码显著紧凑。

方法类型标注

方法的类型标注规则与普通函数一致,可依靠返回语句推断,也可显式标注。fullName()的返回类型由模板字符串推断为stringgreetUserWith()显式标注了string返回类型并给参数标注string

class User { username = "randomString"; protected age!: number; constructor(private firstName: string, private lastName: string) {} fullName() { return `${this.firstName} ${this.lastName}`; } greetUserWith(greeting: string): string { return `${greeting}, ${this.fullName()}`; } } const joe = new User("Joe", "Hiyden"); console.log(joe.fullName()); // "Joe Hiyden" console.log(joe.greetUserWith("Hello")); // "Hello, Joe Hiyden"

访问器(Accessors)类型标注

getter/setter 的参数与返回值同样可以标注类型:

class User { username = "randomString"; protected _age!: number; constructor(private firstName: string, private lastName: string) {} get age(): number { return this._age; } set age(age: number) { this._age = age; } } const joe = new User("Joe", "Hiyden"); joe.age = 20; console.log(joe.age); // 20

访问器类型化有两个值得注意的怪癖:

怪癖一:setter 的参数类型可以从已有 getter 的返回类型推断

当 getter 存在时,setter 的参数类型可以省略——它会自动采用 getter 的返回类型:

class User { username = "randomString"; protected _age!: number; constructor(private firstName: string, private lastName: string) {} // 已有 getter,返回类型 number get age(): number { return this._age; } // setter 参数类型从 getter 返回类型推断 set age(age) { // (parameter) age: number this._age = age; } } const joe = new User("Joe", "Hidin"); joe.age = 20; console.log(joe.age); // 20

怪癖二:只有 getter 的字段被自动视为readonly

如果只有 getter 而没有对应 setter,该属性对实例赋值时会被当作只读:

class User { username = "randomString"; protected _age!: number; constructor(private firstName: string, private lastName: string) {} get age(): number { return this._age; } } const joe = new User("Just", "Kiddin"); // 赋值报只读错误 joe.age = 20; // Cannot assign to 'age' because it is a read-only property.(2540)

另外需要强调:getter 可以标注返回类型,但setter 不允许标注返回类型,以下写法非法:

set age(age: number): number { // A 'set' accessor cannot have a return type annotation.(1095) this._age = age; };

this对象在 TypeScript 类中的处理

JavaScript 中方法所依托的this对象取决于调用点(call site),运行时极易产生不可预期的结果。TypeScript 提供两种手段让this绑定可控、可预测。

用箭头函数永久绑定this

与方法声明不同,箭头函数语法在创建时就把this永久绑定到类实例上:

class User { username = "randomString"; protected age!: number; constructor(private firstName: string, private lastName: string) {} fullName = () => `${this.firstName} ${this.lastName}`; greetUserWith(greeting: string) { return `${greeting}, ${this.fullName()}`; } } const joe = new User("Joe's", "Kiddin"); console.log(joe.fullName()); // "Joe's Kiddin" // 不会丢失 this 上下文,因为已永久绑定到实例 const jfn = joe.fullName; console.log(jfn()); // "Joe's Kiddin"

无论直接在joe上调用还是提取出来稍后调用,fullNamethis始终是joe。代价是:箭头函数没有prototype属性,因此在派生类中无法通过super.fullName()调用它。

this参数做上下文绑定

TypeScript 为每个方法(或访问器)保留了第一个参数位置给this。当需要把实例绑定到方法时,可以把this参数标注为该类类型:

class User { username = "randomString"; protected age!: number; constructor(private firstName: string, private lastName: string) {} fullName(this: User) { return `${this.firstName} ${this.lastName}`; } greetUserWith(greeting: string) { return `${greeting}, ${this.fullName()}`; } } const joe = new User("Joe's", "Hidin"); console.log(joe.fullName()); // "Joe's Hidin" // 脱离上下文调用时报错 const jfn = joe.fullName; console.log(jfn()); // The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'.(2684)

this参数方案适用于确定在User实例上调用、且不会把方法剥离上下文使用的场景;优点是在派生类中可以通过super调用,缺点是方法一旦被提取出上下文就会失去实例绑定(如上例2684错误)。

FAQ:TypeScript 类的常见问题

Q:TypeScript 类与 JavaScript 类有何不同?A:相比 JavaScript 的 ES6 类,TypeScript 类增加了类型标注、访问修饰符、readonly字段、参数属性、泛型,以及多接口实现等能力。

Q:TypeScript 类有私有字段吗?A:有。可以使用privateprotected等修饰符,这是与 JavaScript ES2022#private语法不同的一套机制。

Q:readonly字段的作用是什么?A:创建不可变属性,只允许在初始化或构造器中赋值。

Q:TypeScript 如何在类中绑定thisA:两种途径:一是用箭头函数作为方法以永久绑定this到类实例;二是使用特殊的this参数显式指定类实例类型。

Q:TypeScript 类可以实现多个接口吗?A:可以,使用implements关键字(见下文)。

Q:静态成员能引用类类型参数吗?A:不能,TypeScript 中静态成员无法引用类的类型参数。

Q:参数属性有什么优势?A:可以直接在构造器参数中声明并初始化字段,减少样板代码。

泛型类(Generic Classes)

与泛型类型一样,TypeScript 允许在类声明处传入类型参数,参数类型可用于标注类内任何成员:

class User<T> { readonly userType: T; username = "randomString"; protected age!: number; constructor(userType: T) { this.userType = userType; } } type UserTypes = "Guest" | "Authenticated" | "Admin"; const joe = new User<string>("Guest"); const dae = new User<UserTypes>("Authenticated"); const dan = new User<UserTypes>("Unknown"); // Argument of type '"Unknown"' is not assignable to parameter of type 'UserTypes'.(2345) console.log(joe.userType); // "Guest" console.log(dae.userType); // "Authenticated"

注意,类的类型参数不能传给static成员:

class User<T> { static readonly userType: T; // Static members cannot reference class type parameters.(2302) username = "randomString"; protected age!: number; }

implements实现多个接口

TypeScript 类可以同时实现多个接口,只要类满足接口的全部约定:

interface Identifiable { fullName(): string; } interface Greetable { greetUserWith(greeting: string): string; } interface Updatable { updateUsername(username: string): void; } class User<T> implements Identifiable, Greetable, Updatable { readonly userType: T; username = "randomString"; protected age!: number; constructor( userType: T, private firstName: string, private lastName: string, ) { this.userType = userType; } fullName() { return `${this.firstName} ${this.lastName}`; } greetUserWith(greeting: string) { return `${greeting}, ${this.fullName()}`; } updateUsername(username: string) { this.username = username; } } const joe = new User<string>("Guest", "Joe", "Hidden"); console.log(joe.fullName()); // "Joe Hidden" console.log(joe.greetUserWith("Hello")); // "Hello, Joe Hidden"

如果接口属性未被类满足,TypeScript 报2420错误。例如下面的Registerable接口要求register()方法,而User没有实现它:

interface Identifiable { fullName(): string; } interface Greetable { greetUserWith(greeting: string): string; } interface Updatable { updateUsername(username: string): void; } interface Registerable { register(userId: string): void; } // 因为 User 缺少 register() 方法,报 2420 错误 class User<T> implements Identifiable, Greetable, Updatable, Registerable { // readonly userType: T; username = "randomString"; protected age!: number; constructor( userType: T, private firstName: string, private lastName: string, ) { this.userType = userType; } fullName() { return `${this.firstName} ${this.lastName}`; } greetUserWith(greeting: string) { return `${greeting}, ${this.fullName()}`; } updateUsername(username: string) { this.username = username; } }

类类型之间的关系(结构化类型系统)

TypeScript 基于结构化类型系统(structural typing):只要类(及其实例)的形状一致,就可以相互兼容,不要求名义上的继承关系。

形状相同的类互相兼容

以下示例需在--strictPropertyInitialization: false下运行。UserAdmin形状完全相同,因此实例可互相标注:

// 设置 --strictPropertyInitialization 为 false class User { username = "randomString"; firstName: string; lastName: string; age!: number; } class Admin { username = "randomString"; firstName: string; lastName: string; age!: number; } // User 与 Admin 结构完全一致,互不报错 const joe: User = new Admin(); // joe: User const dona: Admin = new User(); // joe: Admin

子类型对父类型兼容,反之不成立

子类型类拥有父类型全部成员并可能多出额外成员,因此兼容父类型;父类型缺少子类型的成员,不兼容子类型:

class User { username = "randomString"; firstName: string; lastName: string; age!: number; } class Admin { username = "randomString"; firstName: string; lastName: string; age!: number; role: string = "Admin"; } // Admin 实例标注为 User 不报错,因为 User 是 Admin 的子类型 const joe: User = new Admin(); // joe: User // 反过来报错:父类型 User 缺少 role 属性 const dae: Admin = new User(); // Property 'role' is missing in type 'User' but required in type 'Admin'.(2741)

joe能标注为User,因为Admin拥有User的全部成员并多一个role;而dae: Admin不成立,因为User缺少Admin要求的role成员。

真实项目案例:refine 中的KeyBuilder类链

以上语法在真实开源项目中有大量应用。以 refine(一个用于构建内部工具、后台面板与 B2B 应用的 React 框架)核心包中的键构建器为例,源码位于 packages/core/src/definitions/helpers/keys/index.ts,其核心结构如下:

class BaseKeyBuilder { segments: KeySegment[] = []; // 字段声明处初始化 constructor(segments: KeySegment[] = []) { // 参数带默认值 this.segments = segments; } key() { return this.segments; } get() { return this.segments; } } class ParamsKeyBuilder extends BaseKeyBuilder { params(paramsValue?: ParamsType) { return new BaseKeyBuilder([...this.segments, paramsValue]); } } // ...中间多个子类(DataKeyBuilder / AuthKeyBuilder / AccessKeyBuilder / AuditKeyBuilder) export class KeyBuilder extends BaseKeyBuilder { data(name?: string) { return new DataKeyBuilder(["data", name || "default"]); } auth() { return new AuthKeyBuilder(["auth"]); } // ... } export const keys = () => new KeyBuilder([]);

这个案例几乎涵盖了本文讨论的全部要点:

  • 类继承ParamsKeyBuilder extends BaseKeyBuilderKeyBuilder extends BaseKeyBuilder等展示了一层层通过extends扩展职责的类链;
  • 字段声明处初始化segments: KeySegment[] = [];正是"字段在声明处初始化"的典型写法;
  • 构造器参数默认值constructor(segments: KeySegment[] = [])说明构造器参数与普通函数参数一样支持默认值与类型标注;
  • 类创建类型const keyBuilder = keys().auth();keyBuilder的类型由auth()的返回类型决定,而链式调用keys().auth().action("login").params({...})的每一步都返回不同的 Builder 子类;
  • 方法返回类型action()上甚至使用了函数重载签名,为不同的actionType返回不同 Builder 类型(见 index.ts)。

配套的单元测试 packages/core/src/definitions/helpers/keys/index.spec.ts 验证了链式调用的产物:

it("keys().auth().action(login).params({ username: 'test' }) === [auth, login, { username: test }]", () => { const keyBuilder = keys().auth().action("login").params({ username: "test", }); expect(keyBuilder.get()).toEqual(["auth", "login", { username: "test" }]); });

从源码结构可以看出:每个 Builder 子类通过返回"携带了累积路径片段的新 Builder 实例"来实现不可变的链式构建——这正是"类作为类型 + 类创建自身类型 + 构造器参数化"在工程中的真实落地形态。

TypeScript vs JavaScript 类:功能对比

功能特性TypeScript 类JavaScript ES6 类
类型标注支持(字段、方法、参数)不支持
访问修饰符(private等)支持(publicprivateprotected不支持
readonly字段支持不支持
参数属性支持不支持
方法的this参数支持不支持
泛型支持不支持
多接口实现支持(implements不支持

TypeScript 类最佳实践

  • readonly字段守护不可变数据:确保字段在初始化后不再被修改。
  • 善用参数属性:在构造器参数中直接声明并初始化字段,减少样板代码。
  • 使用访问修饰符:用privateprotectedpublic保护成员,保证封装性。
  • 避免滥用静态成员:过度使用静态成员会导致紧耦合。
  • 使用泛型:借助类型参数编写更灵活、可复用的类。
  • 标注方法返回类型:显式声明返回类型让类行为更明确。
  • 测试this绑定:用箭头函数或this参数确保this上下文正确绑定到类实例。

总结

本文完整走过了 TypeScript 类编程的核心内容。我们从字段的类型标注与三种初始化方式出发,认识了确定赋值断言(!)与--strictPropertyInitialization严格初始化;随后梳理了public/protected/private三种可见性、static成员与readonly字段的规则;深入讲解了构造器参数的标注、构造器"无需返回类型"与"类创建自身类型"的特性,以及用参数属性精简字段声明的技巧;还通过示例覆盖了方法、访问器及其参数的类型化,包括 setter 参数类型从 getter 推断、仅 getter 字段自动只读、setter 禁止标注返回类型等怪癖。

针对 JavaScript 中难以预测的this,本文对比了两种 TypeScript 方案:用箭头函数把方法永久绑定到实例,或用this参数做选择性绑定,并说明了各自的代价(箭头函数无法在派生类中通过super调用、this参数在方法脱离上下文时失效)。最后,我们借助泛型类、implements多接口实现,以及结构化类型系统下"同形状类互相兼容、子类型兼容父类型而反之不成立"的规则,理解了类的类型层面的行为,并通过 refine 仓库中真实的KeyBuilder类链(源码、测试)看到了这些特性在生产代码中的协同应用。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:05:32

QEMU CPU建模完全指南:从TCG原理到新增指令集实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:05:32

接收器与混频器深度解析:从原理到故障排查

接收器和混频器这两个词&#xff0c;在射频和音频领域是老面孔了。普通用户可能在蓝牙音频接收模块、无线鼠标接收器这些产品上接触“接收器”多一些&#xff0c;而做通信、做SDR的工程师则天天跟混频器打交道。但很多人其实把这两者的关系想得过于割裂——实际上&#xff0c;绝…

作者头像 李华
网站建设 2026/9/10 5:59:43

fastlane gym 如何配置 ad-hoc 导出方法生成企业内部分发包

fastlane gym 如何配置 ad-hoc 导出方法生成企业内部分发包 【免费下载链接】fastlane &#x1f680; The easiest way to automate building and releasing your iOS and Android apps 项目地址: https://gitcode.com/GitHub_Trending/fa/fastlane 如果你的 iOS 应用不…

作者头像 李华
网站建设 2026/9/10 5:58:19

轻量级规则流路由引擎ruflo:从零实现的架构设计与实践

这几年在折腾后端服务的时候&#xff0c;我越来越觉得&#xff0c;很多逻辑本质上都是在处理同一件事&#xff1a; 根据一堆条件&#xff0c;决定一条数据接下来往哪儿走。 不管是订单状态流转、工单分配、消息推送&#xff0c;还是风控里那一长串“如果...就...”的判断&am…

作者头像 李华