用 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 来解决。或者避开索引签名的语法换用别的方式解决。

相关推荐
渣渣xiong29 分钟前
从零开始:前端转型AI agent直到就业第五天-第十一天
前端·人工智能
布局呆星37 分钟前
Vue3 | 组件通信学习小结
前端·vue.js
C澒40 分钟前
IntelliPro 企业级产研协作平台:前端智能生产模块设计与落地
前端·ai编程
OpenTiny社区2 小时前
重磅预告|OpenTiny 亮相 QCon 北京,共话生成式 UI 最新技术思考
前端·开源·ai编程
前端老实人灬2 小时前
web前端面试题
前端
Moment2 小时前
AI 全栈指南:NestJs 中的 Service Provider 和 Module
前端·后端·面试
IT_陈寒2 小时前
为什么我的JavaScript异步回调总是乱序执行?
前端·人工智能·后端
Moment2 小时前
AI全栈入门指南:NestJs 中的 DTO 和数据校验
前端·后端·面试
小码哥_常2 小时前
告别RecyclerView卡顿!8个优化技巧让列表丝滑如德芙
前端
小村儿3 小时前
Harness Engineering:为什么你用 AI 越用越累?
前端·后端·ai编程