:类型标注、可见性、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/refineTypeScript 类是 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 TypeScriptTypeScript 类通常为其成员以及适用场景下的参数添加类型标注。下面依次覆盖字段、构造器、方法、访问器及其参数的类型化细节。字段类型标注字段的类型标注方式与普通变量一致——有初始值时类型可被推断无初始值时需显式声明。看一个带几个字段的User类class User { username randomString; firstName: string; lastName: string; age!: number; }username的类型由初始值randomString推断为stringfirstName、lastName与age!则显式声明为string/number。其中age后紧跟的!是确定赋值断言运算符definite assignment assertion。字段初始化选项TypeScript 类语法为字段初始化提供了三种选择在声明处初始化、保持未初始化、或未初始化但预期在运行时某个时刻被确定地赋值。User中username属于第一种姓名两个字段属于第二种age!属于第三种。确定赋值断言Definite Field Assignmentsage未初始化但伴随!运算符。它告诉 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 ModifiersTypeScript 为类成员提供public、protected、private三种可见性与 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); // 63private将访问与赋值限制在类内部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; } }需要注意两个关键点构造器自身不能接受类型参数。不存在constructorAbsurdTypeParam() {}这种写法类型参数属于类声明本身用于泛型类见后文class User { // 构造器函数不能接受类型参数 constructorAbsurdTypeParam() {} // 非法 }构造器无需标注返回类型。构造器总是返回实例的类型即类自身创建的那个类型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返回类型并给参数标注stringclass 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(Joes, Kiddin); console.log(joe.fullName()); // Joes Kiddin // 不会丢失 this 上下文因为已永久绑定到实例 const jfn joe.fullName; console.log(jfn()); // Joes Kiddin无论直接在joe上调用还是提取出来稍后调用fullName的this始终是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(Joes, Hidin); console.log(joe.fullName()); // Joes Hidin // 脱离上下文调用时报错 const jfn joe.fullName; console.log(jfn()); // The this context of type void is not assignable to methods this of type User.(2684)this参数方案适用于确定在User实例上调用、且不会把方法剥离上下文使用的场景优点是在派生类中可以通过super调用缺点是方法一旦被提取出上下文就会失去实例绑定如上例2684错误。FAQTypeScript 类的常见问题QTypeScript 类与 JavaScript 类有何不同A相比 JavaScript 的 ES6 类TypeScript 类增加了类型标注、访问修饰符、readonly字段、参数属性、泛型以及多接口实现等能力。QTypeScript 类有私有字段吗A有。可以使用private、protected等修饰符这是与 JavaScript ES2022#private语法不同的一套机制。Qreadonly字段的作用是什么A创建不可变属性只允许在初始化或构造器中赋值。QTypeScript 如何在类中绑定thisA两种途径一是用箭头函数作为方法以永久绑定this到类实例二是使用特殊的this参数显式指定类实例类型。QTypeScript 类可以实现多个接口吗A可以使用implements关键字见下文。Q静态成员能引用类类型参数吗A不能TypeScript 中静态成员无法引用类的类型参数。Q参数属性有什么优势A可以直接在构造器参数中声明并初始化字段减少样板代码。泛型类Generic Classes与泛型类型一样TypeScript 允许在类声明处传入类型参数参数类型可用于标注类内任何成员class UserT { readonly userType: T; username randomString; protected age!: number; constructor(userType: T) { this.userType userType; } } type UserTypes Guest | Authenticated | Admin; const joe new Userstring(Guest); const dae new UserUserTypes(Authenticated); const dan new UserUserTypes(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 UserT { 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 UserT 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 Userstring(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 UserT 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下运行。User与Admin形状完全相同因此实例可互相标注// 设置 --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 BaseKeyBuilder、KeyBuilder 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等支持public、private、protected不支持readonly字段支持不支持参数属性支持不支持方法的this参数支持不支持泛型支持不支持多接口实现支持implements不支持TypeScript 类最佳实践用readonly字段守护不可变数据确保字段在初始化后不再被修改。善用参数属性在构造器参数中直接声明并初始化字段减少样板代码。使用访问修饰符用private、protected、public保护成员保证封装性。避免滥用静态成员过度使用静态成员会导致紧耦合。使用泛型借助类型参数编写更灵活、可复用的类。标注方法返回类型显式声明返回类型让类行为更明确。测试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),仅供参考