likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 3

从一个奇怪的报错讲起

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

interface ObjectWithStrings { [key: string]: string }

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

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

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

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

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...

type MyInterface =  {
    foo: string;
}

declare const myInterface: MyInterface;

requireObjectStrings(myInterface)

不报错了🤯

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

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

尝试解决bug

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

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会将它们合并为一个接口。比如:

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

转载自:https://juejin.cn/post/7349107210658496548
评论
请登录