不满
"说了多少遍要用interface定义组件属性类型,你怎么还在使用type定义组件的属性类型。你自己加一下这个组件的showCount属性的定义,再发包给他。"
我对这个小伙子是越来越不满意了。已经宣贯多少遍了,要用interface定义组件的属性类型,还是使用 type 定义。
起因
部门在开发一个表单控件组件库 wform ,其中包含一个 WInput 组件。这个组件是基于 Ant Design 的 Input 组件进行的二次封装。想使用 Input 组件的 showCount 功能(显示已输入文本的长度)时,向 WInput 组件添加 showCount 属性后发现 TypeScript 报错。这是因为 wInput 组件的属性类型定义 WInputProps 中没有包含 showCount 属性的定义。
A部门的同事向我反馈了这个错误,我看到了,想了一下现在组件库更新版本的流程挺麻烦的,于是对他说:"这个是组件属性类型漏定义了,你可以先使用 declare module 语句来扩展这个组件属性类型。"
"使用 declare module 语句扩展,怎么弄,不太会。"
"好吧,这个方法的确不常用,很简单的,我来教你一下。"
打脸
首先,你需要创建一个.d.ts文件,根据库的名称来命名这个文件,比如 wform.d.ts 。但是要确保这个文件被放置在 tsconfig.json 的 include 字段中指定的目录下。
然后,在wform.d.ts中写入以下代码,其中declare module的参数是一个模块路径,这里最好使用通配符*声明模块路径,避免模块路径错误。
ts
declare module 'wform/*' {
export interface WInputProps {
showCount: boolean;
}
}
最后,见证奇迹。
结果被打脸了。TypeScript 报错依旧存在。不应该啊,开始排查。
排查
打开 tsconfig.json 文件,查看 include 字段的值。
json
{
"include": [
"./src",
]
}
在看 wform.d.ts 文件路径,没错啊,是在工程的src文件夹下。
会不会是引入wInput组件的模块路径不对,查看一下wInput组件引入的代码实现。
tsx
import { WInput } from 'wform';
没错啊,WInput组件引入的模块路径是匹配declare module声明的模块路径。
再观察一下,TypeScript 报错,发现现在是 showCount 属性不报错,其它属性却提示不存在,比如原先的存在 value 属性现在提示不存在。

这应该是通过 declare module 定义的 WInputProps 属性类型,把原先在 WInput 组件中的定义的 WInputProps 属性类型给覆盖了。
罪魁祸首
马上查看了 WInput 组件的代码,发现 WInputProps 属性类型是用 type 来定义的。
ts
export type WInputProps = {
value?: string;
// ...
}
对质
找到 WInput 组件的开发负责人,质问:"为啥不用interface 定义 WInputProps?"
"type和interface都可以用定义对象类型,没啥区别,为啥不行呢。"
"好吧,原来他根本不懂type和interface的区别,真不知道当初怎么把他招进来的。"
type和interface接口的最大区别
使用type定义的类型称为类型别名,interface定义的类型称为接口。
同名类型别名会冲突,同名接口会自动合并。
ts
type UserInfo = {
name: string;
};
// 标识符"UserInfo"重复
type UserInfo = {
age: number;
};
同名接口会自动合并
ts
interface UserInfo {
name: string;
}
interface UserInfo {
age: number;
}
const userInfo: UserInfo = { name: "张三", age: 23 };
userInfo.name; // "张三"
userInfo.age; // 23
在组件库中定义对象比如组件属性时候最好使用 interface,这样方便使用者可以利用 declare module 语句自由地扩展。
接口合并的基本规则
当然接口合并也是有一定规则的,在通常的场景只要考虑以下两点基本规则即可。
-
非函数的成员:如果同名接口包含非函数的成员,这些成员必须是唯一的。如果它们不唯一,那么它们必须是相同的类型。如果不同名的成员有相同的类型,则认为是有效的;如果类型不同,则会导致编译错误。
-
函数成员:对于函数成员,每个同名函数成员都会被视为这个函数的一个重载。TypeScript 会使用这些重载创建一个重载列表。在调用函数时,TypeScript 会尝试使用第一个重载,如果不匹配,就会继续尝试下一个,直到找到一个匹配的重载。