文章目录
- 一、Interface 和 type 有什么区别?如何区分他们的应用场景?
- 1. 核心区别对比
- 2. 功能特性的深度差异
- **A. 声明合并 (Declaration Merging)**
- **B. 定义能力的上限**
- 3. 应用场景归纳:该选哪一个?
- **优先使用 Interface 的场景:**
- **优先使用 Type 的场景:**
- 4. 继承表现的细微差别
- 总结建议
- 二、 映射类型知识点
- 1. 基本语法
- 2. 核心应用场景与示例
- **A. 基础转换:将键映射为特定类型**
- **B. 配合 `keyof`:克隆/修改对象类型**
- 3. 映射修饰符:`readonly` 与 `?`
- **示例:将所有属性变为必填且非只读**
- 4. 键名重映射:`as` 断言
- **示例:生成 Getter 函数类型**
- 5. 内置映射工具类型(Standard Utility Types)
- **`Partial` 的底层实现原理:**
- 总结:为什么要用映射类型?
- 三、declare 关键字的核心作用
- 1. `declare` 的核心作用
- 2. 什么情况下使用它?
- A. 声明全局变量 (Global Variables)
- B. 声明外部模块 (Ambient Modules)
- C. 处理非 JS 资源 (Assets)
- D. 扩展现有对象 (Global Augmentation)
- 3. 代码对比:`declare` vs 正常声明
- 4. 常见误区
- 总结
- 四、 declare不会产生任何实际的 JavaScript 代码是什么意思?
- 1. 深度对比:普通声明 vs. `declare`
- **场景 A:普通声明(有产物)**
- **场景 B:`declare` 声明(零产物)**
- 2. 为什么需要这种“虚空声明”?
- 3. 类型(Shape)是什么意思?
- 4. 总结
- 五、typeScript 中什么是函数重载? 如何实现函数重载?它和使用联合类型的参数有什么不同?
- 1. 如何实现函数重载?
- 示例代码:
- 2. 与联合类型(Union Types)的区别
- 核心区别:**精确的类型关联**
- 对比演示:
- 3. 什么时候用重载?
一、Interface 和 type 有什么区别?如何区分他们的应用场景?
在 TypeScript 中,interface(接口)和type(类型别名)确实非常相似,甚至在很多场景下可以互换。但它们在设计理念、功能特性和扩展性上有着本质的区别。
我们可以通过以下三个维度来深度解析。
1. 核心区别对比
| 特性 | Interface (接口) | Type (类型别名) |
|---|---|---|
| 定义范围 | 只能描述对象结构(Object, Function)。 | 可以定义任何类型(基本类型、联合类型、元组等)。 |
| 同名合并 | 支持。同名接口会自动合并(Declaration Merging)。 | 不支持。定义同名 type 会报错。 |
| 继承方式 | 使用extends关键字。 | 使用&(交叉类型) 模拟继承。 |
| 映射类型 | 不支持。 | 支持。可以基于现有类型生成新类型。 |
2. 功能特性的深度差异
A. 声明合并 (Declaration Merging)
这是interface最独特的特性。如果你定义了两个同名的接口,TypeScript 会自动将它们合并成一个。
interfaceUser{name:string;}interfaceUser{age:number;}// 最终 User 包含 name 和 ageconstperson:User={name:"Tom",age:18};- 用途:常用于扩展第三方库的类型(如在
window对象上挂载全局变量)。
B. 定义能力的上限
type的能力比interface更广,它不仅能定义对象,还能定义“组合拳”。
// 基本类型别名typeID=string|number;// 元组typeData=[number,string];// 联合类型 (Interface 无法直接做到)typeStatus="success"|"error"|"pending";// 映射类型 (Interface 无法做到)typeKeys="name"|"age";typeUserMap={[KinKeys]:string};3. 应用场景归纳:该选哪一个?
优先使用 Interface 的场景:
- 定义 API 接口或对象模型:如果你在定义一个对象的形状(如 User, Product),
interface的语义更贴合“契约”的概念。 - **需要被其他类
implements**:如果你在写面向对象代码,接口是最佳选择。 - 开发第三方库:为了方便库的使用者通过“声明合并”来扩展你的类型,应该暴露
interface。
优先使用 Type 的场景:
- 定义基本类型的别名:如
type Name = string。 - 定义联合类型或交叉类型:如
type Transport = 'Bus' | 'Car'。 - 使用提取、映射等高级类型操作:当你需要用到
keyof、typeof或工具类型(如Partial,Record)进行复杂变换时。 - 定义元组类型。
4. 继承表现的细微差别
- Interface 继承:编译器会检查属性冲突。如果子类和父类属性冲突但类型不兼容,会报错。
- Type 交叉 (
&):它会尝试合并。如果类型冲突(例如一个是string一个是number),该属性会变成never,而不会直接在定义时报错,这可能导致后续调试困难。
总结建议
在团队开发中,业内通用的“最佳实践”通常是:
能用 interface 解决的就用 interface,只有当 interface 无法实现(如联合类型、映射类型)时,才使用 type。
这样可以保持代码风格的统一,同时利用 interface 的性能优势(TypeScript 编译器在处理 interface 的递归引用和缓存时通常比 type 更快)。
二、 映射类型知识点
在 TypeScript 中,映射类型(Mapped Types)是一种非常强大的“类型工厂”。它的核心思想是:基于旧的类型,通过某种规则“加工”出一套新的类型。
如果你熟悉 JavaScript 的Array.prototype.map(),映射类型就是它的“类型空间版”。
1. 基本语法
映射类型通常结合type关键字和in操作符使用。
typeNewType={[KinKeys]:ValueType};K:循环变量,代表每一个属性名。in:遍历操作符。Keys:通常是字符串联合类型(Union Type)或keyof获取的键名集合。
2. 核心应用场景与示例
A. 基础转换:将键映射为特定类型
假设我们有一组权限,需要将其转换为布尔值结构:
typePermissions='read'|'write'|'delete';typePermissionStatus={[KinPermissions]:boolean;};// 结果相当于:// { read: boolean; write: boolean; delete: boolean; }B. 配合keyof:克隆/修改对象类型
这是最常用的场景,基于一个已有的接口生成变体。
interfaceUser{id:number;name:string;age:number;}// 将 User 的所有属性都变为 string 类型typeUserStringfied={[KinkeyofUser]:string;};3. 映射修饰符:readonly与?
在映射过程中,你可以使用+(默认)或-来添加或移除属性的修饰符。
readonly:使属性只读。?:使属性变为可选。
示例:将所有属性变为必填且非只读
interfacePartialUser{readonlyid?:number;readonlyname?:string;}typeConcreteUser={-readonly[KinkeyofPartialUser]-?:string;// -readonly 移除了只读限制,-? 移除了可选(变为必填)};4. 键名重映射:as断言
在 TypeScript 4.1 之后,你可以使用as关键字在映射时修改键名。这在生成模板字符串类型时非常有用。
示例:生成 Getter 函数类型
interfacePerson{name:string;age:number;}typeGetters<T>={[KinkeyofTas`get${Capitalize<string&K>}`]:()=>T[K];};typePersonGetters=Getters<Person>;// 结果:// { getName: () => string; getAge: () => number; }5. 内置映射工具类型(Standard Utility Types)
TypeScript 官方利用映射类型原理,内置了许多非常实用的工具:
Partial<T>:将所有属性变为可选。Readonly<T>:将所有属性变为只读。Pick<T, K>:从 T 中挑选出一组属性 K。Record<K, T>:构造一个键为 K、值为 T 的类型。
Partial的底层实现原理:
// 源码本质就是映射类型typeMyPartial<T>={[PinkeyofT]?:T[P];};总结:为什么要用映射类型?
- 减少重复(DRY):不需要手动维护多个相似的接口(比如一个必填版,一个可选版)。
- 类型安全:当原始接口
User增加属性时,基于它的映射类型会自动更新,不会遗漏。 - 动态生成:可以根据业务逻辑批量修改属性的特征(如统一加前缀、统一变函数)。
一句话理解:映射类型就是类型的循环语句,它让你的类型定义从“静态死板”变得“动态灵活”。</K,></T,>
三、declare 关键字的核心作用
在 TypeScript 中,declare关键字的核心作用是:“告诉编译器,某个变量、类或模块已经存在了,你直接编译就行,不需要在生成的 JavaScript 中创建它。”
它就像是一份“免死金牌”:让 TypeScript 在静态检查阶段闭嘴,不要报错。
1.declare的核心作用
- 类型声明而非实现:
declare只是用来定义类型(Shape),它不会产生任何实际的 JavaScript 代码。 - 弥补缺失的上下文:当你引用的变量不是由当前项目定义的(比如 HTML 中的全局变量、CDN 引入的库),TypeScript 默认会因为找不到定义而报错,
declare用来手动补齐这些定义。
2. 什么情况下使用它?
A. 声明全局变量 (Global Variables)
当你通过<script>标签引入了一些库(如 jQuery),在main.ts中直接使用$会报错。
// 告诉 TS:全局已经有个变量叫 $ 了,它的类型是函数declareconst$:(selector:string)=>any;$('#app').hide();// 现在 TS 不会报错了B. 声明外部模块 (Ambient Modules)
当你安装了一些非常老的库,或者只有.js文件的库,且它们没有提供@types声明文件时。
// 在 typings.d.ts 文件中declaremodule'some-old-utils'{exportfunctiondoSomething(n:number):string;}// 在业务代码中import{doSomething}from'some-old-utils';// 不再报错C. 处理非 JS 资源 (Assets)
在 Vite 或 Webpack 项目中,我们经常import图片或 CSS。但 TS 只认识.ts/js。
// 声明所有的 .png 文件都是模块,防止 import 报错declaremodule"*.png"{constvalue:string;exportdefaultvalue;}D. 扩展现有对象 (Global Augmentation)
比如你想给window对象增加一个自定义属性。
declareglobal{interfaceWindow{__MY_APP_CONFIG__:any;}}window.__MY_APP_CONFIG__={api:'...'};// 安全使用3. 代码对比:declarevs 正常声明
| 动作 | 代码 | 是否生成 JS 代码 | 目的 |
|---|---|---|---|
| 正常声明 | const x = 1; | 是 | 创建并初始化变量 |
declare | declare const x: number; | 否 | 描述环境中已有的变量 |
4. 常见误区
- 它不是“万能药”:
declare只是骗过了编译器。如果你的变量在运行时真的不存在,代码依然会报错(报Uncaught ReferenceError)。 - 不需要初始化:因为
declare不产生 JS,所以你不能给它赋值。
- ❌
declare const x = 1; - ✅
declare const x: number;
总结
当你遇到“Cannot find name ‘xxx’”或者“Property ‘xxx’ does not exist on type ‘window & typeof globalThis’”这种错误,而你又百分之百确定这个东西在运行时是存在的(通常是外部注入的),那就是使用declare的最佳时机。
它是连接“纯 JavaScript 环境”和“TypeScript 类型系统”之间的桥梁。
四、 declare不会产生任何实际的 JavaScript 代码是什么意思?
这个问题触及了 TypeScript 的本质:TypeScript 是 JavaScript 的“马甲”,编译后马甲会脱掉,只剩下肉身(JS)。
所谓“不产生实际的 JavaScript 代码”,我们可以从编译前后的对比来直观理解。
1. 深度对比:普通声明 vs.declare
场景 A:普通声明(有产物)
你在 TS 里写下:
constversion:string="1.0.0";编译成 JS 后:
constversion="1.0.0";// 代码依然存在,占内存结论:这里的const既参与了 TS 的类型检查,也生成了实实在在的 JS 运行逻辑。
场景 B:declare声明(零产物)
你在 TS 里写下:
declareconstversion:string;编译成 JS 后:
// (此处是一片空白,没有任何代码生成)结论:declare告诉编译器:“别操心这个变量怎么来的,我保证运行环境里肯定有一个叫version的字符串。” 编译器听完后,在检查时会放行,但在转换成 JS 时直接把这句话“扔掉”了。
2. 为什么需要这种“虚空声明”?
想象一下,你在 HTML 中通过 CDN 引入了一个地图库(比如高德地图AMap):
- 现实情况:当你运行网页时,浏览器全局环境下确实有
window.AMap。 - TS 的困境:TypeScript 编译器扫描你的本地项目文件,发现没写过
AMap这个变量,它会急得大喊:“Cannot find name 'AMap'!” - 你的对策:你用
declare var AMap: any;。
- 效果:TS 编译器安静了,它知道
AMap存在了。 - 关键点:由于 JS 运行环境里已经有了这个库,TS 编译时绝不能再生成一个
var AMap,否则会覆盖掉原有的地图库。
3. 类型(Shape)是什么意思?
这里的Shape指的是“变量长什么样”。
declarenamespaceMyTool{functionstart():void;letcount:number;}这段代码只是给编译器看的一张说明书(Shape):
- 它告诉 TS:有一个叫
MyTool的东西,它有一个方法叫start,一个属性叫count。 - 它没有真正去内存里分配空间创建一个对象,也没有写出
function start() { ... }的具体逻辑。
4. 总结
- 普通代码= 逻辑(给浏览器执行)+ 类型(给 IDE 检查)。
declare代码= 只有类型。
它的核心语义是“描述已有的”,而不是“创建新的”。
如果你在 TS 里写了declare却忘记在 HTML 或其他地方引入对应的 JS 库,代码在编译时是完美的,但运行时会直接报ReferenceError: xxx is not defined。
五、typeScript 中什么是函数重载? 如何实现函数重载?它和使用联合类型的参数有什么不同?
在 TypeScript 中,函数重载(Function Overloads)是一种允许同一个函数根据接收不同数量或类型的参数,返回不同类型结果的机制。
虽然 JavaScript 本身不支持真正的函数重载(后面的函数会覆盖前面的),但 TypeScript 通过编译时的静态检查模拟了这一特性。
1. 如何实现函数重载?
实现函数重载通常分为两个部分:
- 重载签名(Overload Signatures):定义函数的各种调用方式(不包含函数体)。
- 实现签名(Implementation Signature):包含具体的业务逻辑,且必须兼容所有的重载签名。
示例代码:
假设我们要写一个reverse函数,输入字符串返回字符串,输入数组返回数组。
// --- 重载签名 ---functionreverse(x:string):string;functionreverse(x:any[]):any[];// --- 实现签名 (通用逻辑) ---functionreverse(x:string|any[]):string|any[]{if(typeofx==='string'){returnx.split('').reverse().join('');}else{returnx.slice().reverse();}}// 使用conststr=reverse('hello');// 自动推断为 string 类型constarr=reverse([1,2,3]);// 自动推断为 number[] 类型注意:实现签名对外部是不可见的。在调用时,你只能选择定义的“重载签名”之一。
2. 与联合类型(Union Types)的区别
你可能会问:既然x可以是string | any[],为什么不直接用联合类型?
核心区别:精确的类型关联
| 特性 | 函数重载 | 联合类型 |
|---|---|---|
| 输入输出对应 | 强绑定。输入 A 必返回 A,输入 B 必返回 B。 | 松散。输入 A 或 B,返回结果通常是 `ResultA |
| 语义化 | 非常清晰。开发者能一眼看出哪些组合是合法的。 | 逻辑混合。需要开发者自己判断参数组合。 |
| 代码提示 | 编辑器会根据参数给出精确的返回类型建议。 | 返回值通常需要手动类型断言或二次判断。 |
对比演示:
使用联合类型:
functioncombine(x:string|number,y:string|number):string|number{return(typeofx==='number'&&typeofy==='number')?x+y:`${x}${y}`;}constresult=combine(1,2);// result 的类型是 string | number缺点:虽然我们知道传入两个数字会得到数字,但 TS 编译器此时只知道结果是string | number,你可能需要用as number强转。
使用函数重载:
functioncombine(x:number,y:number):number;functioncombine(x:string,y:string):string;functioncombine(x:any,y:any):any{returnx+y;}constresult=combine(1,2);// result 类型精确为 number优点:编译器能够根据你传入的1, 2锁定第一个重载签名,直接确定返回值为number。
3. 什么时候用重载?
- 当你需要根据参数类型改变返回类型时(如上面的
reverse例子)。 - 当你需要根据参数数量改变返回类型时。
- 库开发:为了给用户提供最完美的 API 文档和自动补全。
小贴士:如果你的函数逻辑非常简单,且不同参数返回的类型相同,优先使用联合类型或泛型,因为重载会让代码变得冗长。只有在返回类型取决于输入类型时,重载才是真正的“大杀器”。