别再被 TS 类型冲突折磨了!一文搞懂类型合并规则

之前学习了TypeScript的类型定义,我们都知道开发语言中的变量会有覆盖声明的情况,那么对于类型定义是不是也会有这种情况,那么该如何正确利用这种合并规则?在遇到多个类型定义的时候,我们又该如何处理?

了解类型合并规则,有助于我们定义类型,避免类型冲突。可以利用这种合并规则,更灵活的定义类型。

不同版本TypeScript,不同配置可能会导致合并差异,这里说明使用的版本为"typescript": "^5.9.3",开启了配置"strict": true,测试文件后缀为.ts.d.ts文件可能更为宽松。

同名同类型合并

TypeScript仅支持接口interface、命名空间namespace、函数声明function同名合并。在声明解析阶段即完成合并。早于其他类型引用处理,比如交叉/联合类型。

对于typeclassenum等定义的同名类型都不能合并。

interface 接口声明合并

同一作用域下的同名接口会自动完成合并,无需额外语法。

合并特性:

  • 属性、方法、索引签名均可合并。
  • 同名属性、方法必须类型兼容,否则编译器报错。
  • 同一作用域/模块下。
ts 复制代码
interface Animal {
  name: string;
}

interface Animal {
  age: number;
}

// 实例必须包含name和age属性
const dog: Animal = {
  name: "",
  age: 0,
};

同属性,不同类型不兼容,编译器报错。

ts 复制代码
interface Animal {
  name: string;
}
// ❌ 后续属性声明必须属于同一类型。属性"name"的类型必须为"string",但此处却为类型"number"。
interface Animal {
  name: number;
}

同属性同类型,不同修饰符不兼容,编译器报错。

ts 复制代码
interface Animal {
  name: string;
}
// ❌ 不同修饰符不兼容,编译器报错
interface Animal {
  name?: string;
}

namespace 命名空间合并

同名的命名空间会自动合并内部导出的成员。仅export导出的成员会合并。

ts 复制代码
namespace Utils {
  export function getName() {
    return "hboot";
  }
}

namespace Utils {
  export function getAge() {
    return 18;
  }
}

Utils.getName();
Utils.getAge();

不允许同名成员导出。

ts 复制代码
namespace Utils {
  export function getName() {
    return "hboot";
  }
}

namespace Utils {
  // ❌ 成员已存在,不允许重复定义
  export const getName = () => {
    return 18;
  };
}

function 函数声明合并

函数同名合并,我们称之为函数重载。TS编译器会按照倒序匹配,也就是后声明函数重载优先级高。

函数重载最后一个函数声明必须实现内部逻辑,并且参数数量、参数类型和返回值类型必须兼容。

仅使用function声明的函数支持

ts 复制代码
function getName(name: string): string;
function getName(age: number): number;
function getName(nameOrAge: string | number): string | number {
    return nameOrAge;
}
getName("hboot");
getName(18);

interface接口中定义的方法也会形成重载。但是和普通的函数重载最后的函数实现参数、返回值类型定义有些不同。

ts 复制代码
interface Animal {
  getVal(name: string): string;
}
interface Animal {
  getVal(age: number): number;
}

// ❌ 这样写编译器会直接报错,提示不能将类型 "string | number" 无法分配给类型 "string"。
const dog: Animal = {
  getVal(value: string | number): string | number {
    return value;
  },
};

普通函数重载是满足其一即可;而接口中方法重载是必须精准匹配类型每一个类型。利用TS类型推导通过泛型参数锁定类型。

ts 复制代码
// 利用了类型的自动推导,通过泛型参数 T 锁定输入类型及返回类型
const dog: Animal = {
  getVal<T extends string | number>(value: T): T {
    return value;
  }
};

同名不同类型合并

同名不同类型的合并,主要是命名空间namespace + interface/class/function的合并,namespace可以提供静态属性、方法。

namespace+interface 合并

同名的namespace命名空间为interface接口扩展静态成员;接口提供类型约束。

ts 复制代码
interface Animal {
  name: string;
}

namespace Animal {
  export const age = 18;
  export function getName() {
    return "hboot";
  }
}

const dog: Animal = {
  name: "hboot",
};

Animal.getName();

同名的属性、方法不会冲突,因为Animal命名空间直接通过空间名访问;interface接口需要实例化后的实例访问。它们之间实际上只有名称是相同的,属性之间没有合并。

namespace+class 合并

同名的namespace命名空间为class类扩展静态成员;类声明必须在命名空间的声明必之前,命名空间不能声明类已有的成员。

ts 复制代码
class Animal {
  name: string;
  static age: number;
  constructor(name: string) {
    this.name = name;
  }
}
namespace Animal {
  export const name = "hboot";
  // ❌ 此处扩展静态成员 age 报错,类中已存在 age 静态成员
  export const age = 18;
  export function getName() {
    return "hboot";
  }
}

const dog: Animal = new Animal("admin");
Animal.getName();
// hboot
Animal.name;
// admin
dog.name;

namespace+function 合并

同名的namespace命名空间为function函数扩展静态成员。函数保持自身的可调用能力。

ts 复制代码
function speak(name: string) {
  return "Hello World! " + name;
}

namespace speak {
  // ❌ 此处无法覆盖 函数的 name 属性;name 是只读属性
  // ❌ 类型校验没有报错,但运行时因为只读而报错
  export const name = "hboot";
  export function getName() {
    return "hboot";
  }
}

speak("hboot");
speak.name;
speak.getName();

扩展的静态成员最好不要覆盖函数本身的属性,比如namelength等。这些只读属性无法被覆盖,在运行时会报错。

interface+class 合并

同名的interface接口为class类扩展实例成员。类继承接口的属性、方法,实例必须同时满足接口和类的约束。

合并特性:

  • 接口的必选属性,在类中必须显式实现,否则执行报错。
  • 同名属性,必须类型兼容,否则执行报错。
ts 复制代码
interface Animal {
  name: string;
  getName(): string;
}

class Animal {
  age: number;
  constructor(age: number, name: string) {
    this.age = age;
    this.name = name;
  }
  // 必须显示实现 接口 的方法成员
  getName() {
    return this.name;
  }
}

const dog: Animal = new Animal(18, "hboot");

dog.age;
// 类中需通过构造函数
dog.name;
// ❌ 如果类没有显示实现;智能提示存在方法,实际调用会报错。
dog.getName();

除了手动赋值扩展属性外,可以通过public修饰符自动生成

ts 复制代码
class Animal {
 
  constructor(age: number, public name: string) {
    this.age = age;
    // 无需手动赋值
    // this.name = name;
  }

  const dog: Animal = new Animal(18, "hboot");
  // ...
}

显示类型合并

上述的同名同类型、同名不同类型合并实际最终也是属性的合并。对于非同名属性合并则是扩展;同名属性则有一些合并规则。

对于不同类型之间的同名属性合并都有自己的规则,比如:interface+class 合并要求属性类型兼容;namespace+class 合并要求命名空间不能包含类已有的成员。

通过手动将一些类型合并到一个类型中,例如交叉类型&和联合类型|

交叉类型& 关系合并

交叉类型将多个类型合并为一个新类型。新类型必须满足所有类型约束。

合并特性:

  • 不同名属性合并为属性并集。保留属性修饰符。
  • 同名属性取兼容类型,对于修饰符?,存在必选时则属性必选;修饰符readonly,存在属性可修改时则属性可修改。
ts 复制代码
// 不兼容类型 never
type A = string & number;

// 不同名属性并集
type B = { name: string } & { age: number };

// 可选 ? 修饰符, 兼容类型 name 为必选属性
type C = { name?: string } & { name: string };

// 只读 readonly 修饰符, 兼容类型 age 为可修改
type D = { readonly age: number } & { age: number };

联合类型| 关系合并

将多个类型组合为一个新类型。新类型只需要满足其中一个类型约束。

合并特性:

  • 仅能访问公共属性。需通过类型守卫收窄类型后才能访问非公共属性。
  • 完全一致的类型自动去重。
ts 复制代码
type A = {
  name: string;
  age: number;
};

type B = {
  name: string;
  address: string;
};

type C = A | B;

// 满足其中一个类型约束
const c: C = {
  name: "hboot",
  age: 18,
};

// 访问非公共属性
function viewC(data: C) {
  if ("age" in data) {
    return data.age;
  }
  return data.address;
}

.d.ts中的声明合并

.d.ts文件和.ts文件的合并核心规则一致。在执行时机、作用域、编译行为上有些不一样。

  • .d.ts 文件中仅有类型声明,无实际代码实现,仅用于TS类型检验。所以不同于.ts文件。它会在类型校验阶段早期执行,优先合并全局/模块类型;而.ts文件是在编译阶段执行。

  • .d.ts类型优先级低,能被.ts文件显示类型声明覆盖。

  • .d.ts跨文件同名声明合并(无import/export)。.ts仅在同一个文件中同名声明合并。

declare

declare 主要作用存在性声明,告诉TypeScript编译器无需生成对应的代码。

  • 全局变量/函数声明,比如:外部加载的js文件,挂载到window上的变量。
  • 扩展已有类型(与同名接口/命名空间合并)
  • 声明模块(非TS模块,比如.css.png等静态资源)

declare 扩展已有类型合并规则与.ts文件的合并核心规则一致.

ts 复制代码
interface Animal {
  name: string;
  getName(): string;
}
declare interface Animal {
  age: number;
}

.ts 模块中没有import/export时,通过declare声明为全局作用域。如果存在import/export则需要通过declare global扩展全局作用域。

相关推荐
qingyun9892 小时前
Web Components 实战:创建自定义比例条组件
前端
前端小超超2 小时前
ionic + vue3 + capacitor遇到backButton问题
前端·javascript·vue.js
GIS之路2 小时前
GDAL 空间关系解析
前端
布列瑟农的星空2 小时前
WebAssembly入门(一)——Emscripten
前端·后端
贵州数擎科技有限公司2 小时前
一批优质 AI 域名转让(.ai)|适合 AI 创业 / 产品 / 公司品牌
前端
小二·2 小时前
微前端架构完全指南:qiankun 与 Module Federation 双方案深度对比(Vue 3 + TypeScript)
前端·架构·typescript
EndingCoder3 小时前
枚举类型:常量集合的优雅管理
前端·javascript·typescript
Electrolux3 小时前
[wllama]纯前端实现大语言模型调用:在浏览器里跑 AI 是什么体验。以调用腾讯 HY-MT1.5 混元翻译模型为例
前端·aigc·ai编程
sanra1233 小时前
前端定位相关技巧
前端·vue
起名时在学Aiifox3 小时前
从零实现前端数据格式化工具:以船员经验数据展示为例
前端·vue.js·typescript·es6