用 interface 会报错改成 type 就好了?从一个报错深入 typescript 中 interface 和 type 的区别

从一个奇怪的报错讲起

在 TypeScript 中, { [props: string]: any } 是一个索引签名的语法,表示一个对象可以有任意数量的属性.比如,我要表达一个 value 是 string 的对象:

vbnet 复制代码
interface ObjectWithStrings { [key: string]: string }

这种写法非常常见,假设我们有一个方法叫 requireObjectStrings,大致抽象一下:

typescript 复制代码
interface ObjectWithStrings { [key: string]: string }
declare const requireObjectStrings: <T extends ObjectWithStrings>(object: T) => void;

这个函数非常简单,用人话描述一下:函数 requireObjectStrings 接受一个对象作为参数,这个对象的属性名和属性值都必须是字符串类型,返回值为 void。

看起来没什么问题,写一个测试用例试试:

php 复制代码
interface MyInterface  {
    foo: string;
}

declare const myInterface: MyInterface;

requireObjectStrings(myInterface)

看起来也没什么问题...等等,ts 编译报错了!

你可以在这里看到 demo

报错信息为:

Argument of type 'MyInterface' is not assignable to parameter of type 'ObjectWithStrings'.

Index signature for type 'string' is missing in type 'MyInterface'.

报错信息有点不太能理解,尝试改一下代码试试,比如把 interface 改成 type...

ini 复制代码
type MyInterface =  {
    foo: string;
}

declare const myInterface: MyInterface;

requireObjectStrings(myInterface)

不报错了🤯

突然有一种一顿瞎改之后代码能正常运行,但是不知道为什么能正常运行的感觉...

尝试解决bug

对于一个公共库来说,我们不能限制用户说你只能用 type 来声明范型。有什么办法解决吗? 上面的 ts 编译报错与索引签名有关,那我试试换个方式来解决:

typescript 复制代码
interface ObjectWithStrings { [key: string]: string }
declare const requireObjectStrings: <T extends {[K in keyof T]: string}>(object: T) => void;

interface MyInterface  {
    foo: string;
}

declare const myInterface: MyInterface;

requireObjectStrings(myInterface)

使用 keyof 关键字,{[K in keyof T]: string} 来约束范型。

Great!编译通过。

bug背后: typescript 中 interface 和 type 的区别

网络上关于 interface 和 type 的区别有很多解释,在这里不赘述了。这个 bug 和 interface 和 type 有什么关系呢?我顺藤摸瓜找到了这个 issue: typescript 的维护者回答道:

Just to fill people in, this behavior is currently by design. Because interfaces can be augmented by additional declarations but type aliases can't, it's "safer" (heavy quotes on that one) to infer an implicit index signature for type aliases than for interfaces. But we'll consider doing it for interfaces as well if that seems to make sense

翻译一下:目前这种行为是有意为之的。因为 interface 可以通过额外声明来增强,但type 不行,所以对 type 推断隐式索引签名比对接口推断要"更安全"(这里的"更安全"要打很多引号)。不过,如果这样做对 interface 也有意义的话,我们也会考虑对 interface 进行类似的推断。

这段话的要点是: 这段话指出了它们之间的一个关键区别:

interface 可以被扩展,意味着你可以在程序的不同部分为同一个接口添加新的属性,TypeScript会将它们合并为一个接口。比如:

css 复制代码
interface Person {
  name: string;
}

interface Person {
  age: number;
}

let p: Person = {
    name: 'li',
    age: 8,
}

以上 Person 类型会被合并。 type 则不可被扩展,一旦定义了 type,它就是固定的,不能再添加新的属性。

因为 type 不可扩展,TypeScript的类型系统在处理类型别名时会更加"宽松",它会对 type 隐式地推断出索引签名。所以 interface 会出现报错,而 type 不会。

上面这段解释可能有点刻意,其实社区的很多讨论认为这个特性算是 typeScript 的 bug。typeScript 的设计在很多地方也是考虑到了实用性和健全性的妥协。如文档所说:

TypeScript's type system allows certain operations that can't be known at compile-time to be safe. When a type system has this property, it is said to not be "sound". The places where TypeScript allows unsound behavior were carefully considered, and throughout this document we'll explain where these happen and the motivating scenarios behind them.

typeScript 的类型系统并不是完全健全的。在类型系统的术语中,"健全"(sound)意味着类型系统能够保证所有的程序都不会在运行时出现类型错误。上面的怪异行为也算是 typeScript 类型系统不健全的一个体现。

总之,如果你要是遇到类似上面的问题,可以选用更安全的 type 而不是 interface 来解决。或者避开索引签名的语法换用别的方式解决。

相关推荐
Hilaku2 分钟前
为什么 Cloudflare 能做到毫秒级冷启动,而其它云厂商却不行?
前端·javascript·程序员
2301_7944615710 分钟前
JavaScript 基础知识点详解
前端·javascript·html
HjhIron13 分钟前
在浏览器中跑DeepSeek-R1:用React+WebGPU实现端侧AI推理
前端·ai编程
早期的虫儿有鸟吃26 分钟前
vue2--Vuex 模块化
开发语言·前端·javascript
程序员清风32 分钟前
推荐几个我常听的AI播客!
java·后端·面试
C++、Java和Python的菜鸟41 分钟前
第5章 后端Web基础 (MySQL基础)
前端·mysql·adb
门前大桥下.1 小时前
HTML-01我的第一个网页
前端·html
我是大卫1 小时前
【图】React源码解析-从数据结构、依赖追踪机制、值传播与更新触发、以及性能陷阱与优化,深挖useContext的底层原理
前端·react.js·源码
谭光志1 小时前
深入浅出 RAG:用一个可运行的 Demo 讲透完整链路
前端·后端·ai编程
Cobyte1 小时前
使用 JavaScript 实现有限状态机的经典问题
前端·javascript·vue.js