前言
你是否遇到过这样的情况:明明传了一个「看似不对」的类型TS
居然不报错?比如下面的代码:
ts
class JS {
code() { }
}
class TS {
code() { }
}
function useJS(js: JS) {
return js.code
}
useJS(new TS()) // 正常运行
这!不!科!学! 😱
这不是TypeScript
的 bug,而是一个精心设计的特性------结构化类型 (Structural Typing),江湖人称鸭子类型 (Duck Typing)。这个看似「随意」的机制,其实是TypeScript
灵活性的核心密码。它让代码像乐高积木般自由组合,但也可能让刚接触的你满头问号:
- 为什么两个毫无关系的类可以互相赋值?
- 怎么避免「长得像」的类型意外兼容?
- Java/C# 那套类型规则在
TypeScript
里为何失效?
这篇文章将用 5 分钟带你拆解结构化类型的底层逻辑,通过: 🔥 直击灵魂的代码案例 🦆 鸭子类型的趣味解读 ⚔️ 结构化 vs 名义类型的世纪对决 🔧 模拟名义类型的实战技巧
让你不仅看懂现象,更能掌握类型兼容性的「潜规则」,从此告别类型错乱的玄学问题!
结构化类型
回到上面的问题,我们可以看出,即使我们方法需要的是JS
,传入TS
进去也不会报错,会不会是因为它们的属性方法一致导致的呢?
我们给JS
类添加一个独有的方法
ts
class JS {
code() { }
extraMethod() { }
}
class TS {
code() { }
}
function useJS(js: JS) {
return js.code
}
/**
* 类型"TS"的参数不能赋给类型"JS"的参数。
* 类型 "TS" 中缺少属性 "extraMethod",但类型 "JS" 中需要该属性
*/
useJS(new TS())
可以看到,编译器直接给出了报错提示,这是因为
在TypeScript
中,只要对象的结构(属性和方法)符合某个类型,它就可以被当作该类型,即 只看结构,不看名字。
例子中可以看出,实际上是比较了JS
类型上的方法与属性是否都存在于TS类型上。
回到最开始的例子,虽然它们是两个名字不同的类型,但是里面的属性方法是一致的,所以视为结构一致。
这时可能会有小伙伴产生疑问,给JS
类添加独特方法会报错,如果 TS
类在 JS
的基础上增加额外方法,会发生什么?
ts
class JS {
code() { }
}
class TS {
code() { }
extraMethod() { }
}
function useJS(js: JS) {
return js.code
}
useJS(new TS()); // ✅ 允许
居然没有报错!
这是因为,结构化类型 ,也叫 鸭子类型,它的核心思想是:
"如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子!"
它的核心规则是:
- 如果对象的属性、方法匹配某个类型,即可视为该类型 ,而不考虑类型名称。
- 这种方式提高了代码的灵活性 ,但在某些情况下,也可能导致意外的类型兼容性问题 。 如果我们想强制区分类型 ,该怎么做?这就涉及 名义类型系统。
名义类型
在 Java 或 C# 中,即使两个类的属性完全相同,如果它们没有继承同一个父类或实现同一个接口,仍然 不能 互相赋值
ts
class Person {
String name;
int age;
}
class User {
String name;
int age;
}
Person p = new User(); // ❌ 报错,虽然结构相同,但类型不同
即使它们"长得一样",也不能直接赋值,这就体现了名义类型的严格性。
名义类型(Nominal Typing) :类型必须 显式声明关系(如继承或实现接口),否则即使结构相同也不兼容。
- 类和接口的兼容性取决于显式继承关系。
- 避免了意外的类型匹配,增强了类型安全性。
- 常见于 Java、C#、Go 等静态类型语言。
那么我们是否可以让TypeScript
去实现名义类型系统呢?
TypeScript 如何实现名义类型
在TypeScript
里,类的实例也遵循结构化类型 ,但类的 私有(private
)和受保护(protected
)成员 不能参与兼容性比较。
我们可以利用这点,去实现TypeScript
的名义类型
1. 使用private
字段(较适用于类)
ts
class JS {
private t: number
code() { }
}
class TS {
private t: number
code() { }
}
function useJS(js: JS) {
return js.code
}
useJS(new TS()) // ❌ 报错,TS 不能赋值给 JS
- 仅适用于类实例,无法作用于接口、类型别名。
- 需要在类中手动声明
private
字段,稍显繁琐。
2. 我们还可以使用Brand<T, U>
(更灵活)
ts
// 定义一个 Brand 类型,给类型打上唯一标识
type Brand<T, U> = T & { __brand: U };
type JS = Brand<{ code: () => void }, "JS">;
type TS = Brand<{ code: () => void }, "TS">;
function useJS(js: JS) {
return js.code;
}
const jsInstance = { code: () => console.log("JS") } as JS;
const tsInstance = { code: () => console.log("TS") } as TS;
useJS(jsInstance); // ✅ 允许
useJS(tsInstance); // ❌ 报错,TS 不能赋值给 JS
- 适用于 任意类型(包括接口、对象类型等)。
- 通过泛型
U
作为唯一标识,避免类型误用。 - 代码更清晰,适用于大规模项目。
通过这种方式模拟出了名义类型,我们可以添加更多的检查逻辑,保障类型安全
结构化类型 vs 名义类型
特性 | 结构化类型 | 名义类型 |
---|---|---|
兼容性判断 | 结构匹配即合法 | 必须显式继承或实现接口 |
灵活性 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
类型安全 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
典型语言 | TypeScript, Go | Java, C#, Swift |
总结
✅ TypeScript
采用结构化类型系统 ,只关心 对象的结构是否匹配 ,不关心 类型的名称。
✅ 比 Java、C# 更灵活(它们采用名义类型,需要继承或实现相同接口)。
✅ 对象赋值、函数参数、接口、类的兼容性 都遵循 结构化类型,提高代码可复用性。
✅ private
/protected
属性会触发 名义类型检查,成为结构化规则的特例
感谢阅读!欢迎
点赞
、收藏
、关注
,一键三连!!!