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

相关推荐
东风破_2 小时前
danci 项目(三):从 JSON 数据到 AI Coding,真实项目里的数据清洗、Prompt 和工程规范
前端·后端·node.js
怕浪猫3 小时前
FDE 最大的浪费不是写出有 bug 的代码,而是漂亮地解决了一个错误的问题
面试·架构·github
东风破_3 小时前
danci 项目(一):从需求到架构,一个单词学习系统为什么会这样设计
前端·后端·node.js
蒲公英eric3 小时前
从客户端到服务端:DVWA DOM 型 XSS 模块完整漏洞分析教程
前端·web安全·ai·xss·dvwa·ai安全
单线程_013 小时前
从案例分析 Vue3 Tokenizer+Parser 源码三
前端·javascript·vue.js
小磊哥er4 小时前
深入解构Claude Code - 第 10 篇 · 高级能力
typescript·ai编程
顶点多余4 小时前
那些在算法中适合巩固的知识点---1
java·前端·算法
Setsuna_F_Seiei4 小时前
前端转型 Agent 开发 03 之 Agent Tools - 给 Agent 装上手脚
前端·agent·ai编程
jay神4 小时前
【计算机毕业设计】基于SpringBoot的程序教学辅助系统
java·前端·vue.js·spring boot·后端·毕业设计·课程设计
小磊哥er4 小时前
深入解构Claude Code - 第 9 篇 · 怎么给它加功能
typescript·ai编程