5 分钟掌握 TypeScript 结构化类型系统,一次搞懂鸭子类型!

前言

你是否遇到过这样的情况:明明传了一个「看似不对」的类型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 属性会触发 名义类型检查,成为结构化规则的特例

感谢阅读!欢迎点赞收藏关注,一键三连!!!

相关推荐
烛阴39 分钟前
秒懂 JSON:JavaScript JSON 方法详解,让你轻松驾驭数据交互!
前端·javascript
拉不动的猪1 小时前
刷刷题31(vue实际项目问题)
前端·javascript·面试
zeijiershuai1 小时前
Ajax-入门、axios请求方式、async、await、Vue生命周期
前端·javascript·ajax
只会写Bug的程序员1 小时前
面试之《webpack从输入到输出经历了什么》
前端·面试·webpack
拉不动的猪1 小时前
刷刷题30(vue3常规面试题)
前端·javascript·面试
狂炫一碗大米饭1 小时前
面试小题:写一个函数实现将输入的数组按指定类型过滤
前端·javascript·面试
最胖的小仙女1 小时前
通过动态获取后端数据判断输入的值打小
开发语言·前端·javascript
yzhSWJ2 小时前
Vue 3 中,将静态资源(如图片)转换为 URL
前端·javascript·vue.js
Moment2 小时前
🏞 JavaScript 提取 PDF、Word 文档图片,非常简单,别再头大了!💯💯💯
前端·javascript·react.js
Aphasia3112 小时前
Web身份认证与状态管理:Cookie、Session 与 JWT
前端·面试