如何让TypeScript类型包含子类型且自身可作为有效类型
在TypeScript中创建兼具自身类型与嵌套子类型的结构
需求说明
我希望创建一个TypeScript结构(namespace/class/interface/type均可),使其既包含属性与方法的类型声明,又能拥有类似Character.Stats或Character.Currency的嵌套子类型,同时Character本身仍可作为有效类型使用。
我需要让以下classes.ts代码合法:
// 导入Character类型的语句 class Player implements Character { constructor ( public name: string, // 属性 public stats: Character.Stats, // 子类型标注的属性 public currency: Character.Currency ) { this.name = name this.stats = stats this.currency = currency } attack(target: Player) { // 参数为自身类的类型 console.log(`Attacked ${target.name}`) } }
这在当前TypeScript中是否可行?
当前测试代码结构
types/items.ts
export interface Type { name: string, sprite: string, }
types/characters.ts
import * as Classes from "../classes"; export interface Stats { health: number, attack: number, defence: number, } export interface Currency { gold: number, ridium: number } export interface Type { name: string, stats: Stats currency: Currency announce(sentence: string): void, attack(target: Type): void, heal(item: Classes.HealingItem): void }
classes.ts
import * as Characters from "./types/characters"; import * as Items from "./types/items" export class Character implements Characters.Type { constructor( public name: string, public stats: Characters.Stats, public currency: Characters.Currency ) { this.name = name; this.stats = stats this.currency = currency } announce(sentence: string) { console.log(`I, ${this.name}, ${sentence}`) } attack(target: Character): void { this.announce(`am going to attack ${target.name}`) } heal(item: HealingItem) { this.stats.health += item.healthPoints; this.announce(`used ${item.name} to heal my hp from ${this.stats.health - item.healthPoints} to ${this.stats.health}`) } } export class HealingItem implements Items.Type { constructor( public name: string, public sprite: string, public healthPoints: number ) { this.name = name this.sprite = sprite this.healthPoints = healthPoints } }
index.ts
import * as Classes from "./classes"; const hero = new Classes.Character("Bob", // name { // stats health: 100, attack: 10, defence: 25 }, { // currency gold: 50, ridium: 0 } ) const apple = new Classes.HealingItem("Apple", "./sprites/apple.sprite", 25); hero.heal(apple); hero.attack(hero);
当前代码可正常运行,但我认为用Items.Type/Characters.Type的命名方式未来可能引发混淆或维护问题。我想要的理想效果如下(注:以下代码不合法,仅用于表达需求):
interface B { x: string } namespace A { export type v1 = number export type v2 = boolean export type v3 = B } let p: A = { v1: 9, v2: true, v3: { x: "some string" } }; let q: A.v1 = 2; let r: A.v2 = false; let s: A.v3 = { x: "strung" };
解决方案:利用TypeScript声明合并
这在TypeScript中完全可行,核心是使用声明合并特性——将同名的接口(作为主类型)与命名空间合并,从而实现:
- 接口作为
Character自身的类型定义 - 命名空间承载
Stats、Currency等嵌套子类型
修改后的types/characters.ts
// 使用类型导入避免循环依赖 export interface Character { name: string; stats: Character.Stats; currency: Character.Currency; announce(sentence: string): void; attack(target: Character): void; heal(item: import("../classes").HealingItem): void; } // 同名命名空间,与上方接口合并 export namespace Character { export interface Stats { health: number; attack: number; defence: number; } export interface Currency { gold: number; ridium: number; } }
修改后的classes.ts
import { Character } from "./types/characters"; import { Type as ItemType } from "./types/items"; export class Player implements Character { constructor( public name: string, public stats: Character.Stats, public currency: Character.Currency ) {} attack(target: Player) { console.log(`Attacked ${target.name}`); } announce(sentence: string): void { console.log(`I, ${this.name}, ${sentence}`); } heal(item: HealingItem): void { this.stats.health += item.healthPoints; this.announce(`used ${item.name} to heal my hp from ${this.stats.health - item.healthPoints} to ${this.stats.health}`); } } export class HealingItem implements ItemType { constructor( public name: string, public sprite: string, public healthPoints: number ) {} }
关键说明
- 声明合并规则:同名的接口和命名空间会被TypeScript自动合并,外部既可以将
Character当作类型使用(如implements Character、let char: Character),也可以通过Character.Stats访问嵌套子类型。 - 避免循环导入:如果主接口需要引用外部类(如
HealingItem),使用import("../classes").HealingItem这种类型导入语法,可避免文件间的循环依赖问题。 - 命名空间成员导出:命名空间内的子类型必须用
export修饰,才能被外部访问。
验证效果
修改后,你最初目标中的classes.ts代码将完全合法,同时满足:
Player类正确实现Character类型- 可以用
Character.Stats、Character.Currency标注属性类型 Character本身可作为有效类型使用
内容的提问来源于stack exchange,提问作者Matthew Basford
相关产品推荐
相关产品推荐

