React 组件 props 的 TypeScript 类型或接口
在使用 TypeScript 编写 React 组件时,在为其 props 赋值时有两种选择:类型别名或接口。你会选择哪一种?这重要吗?真的存在最优选择吗?这两种方式各有优缺点吗?在得出结论之前,让我们先探讨一下它们之间的相关区别。
本文并非深入探讨 TypeScript 中类型别名和接口的区别,但我将简要概述一些与 React props 相关的差异,以便大家理解。稍后我们将探讨这些差异在本文的语境下是如何发挥作用的。
类型别名与接口
TypeScript 中的类型别名和接口在大多数情况下是等效的。在其中一个中可以实现的功能,在另一个中也可以实现,只需更改语法即可,当然也有例外。
让我们看一些例子:
具有属性的常规对象
✔ 同等
type User = {
name: string
}
// ...
interface User {
name: string
}
数组或索引器
✔ 同等
数组或索引器:
type Users = User[]
// ...
interface Users {
[index: number]: User
}
👆 不过,在这种情况下,接口将缺少所有数组方法,如.push
,.map
等,因此两个定义并不完全等同,并且除非这正是您的目标,否则接口的用处就没那么大了。
为了解决这个问题,您必须明确地从数组类型扩展,如下所示:
type Users = User[]
// ...
interface Users extends Array<User> {
[index: number]: User
}
功能
✔ 同等
type GetUserFn = (name: string) => User
// ...
interface GetUserFn {
(name: string): User
}
函数重载并添加属性
✔ 同等
让我们使用一个真实世界的例子,这是it: TestFunction
来自的类型定义mocha
,请参阅此处的源代码。
type TestFunction =
& ((fn: Func) => Test)
& ((fn: AsyncFunc) => Test)
& ((title: string, fn?: Func) => Test)
& ((title: string, fn?: AsyncFunc) => Test)
& {
only: ExclusiveTestFunction;
skip: PendingTestFunction;
retries(n: number): void;
};
// ...
interface TestFunction {
(fn: Func): Test
(fn: AsyncFunc): Test
(title: string, fn?: Func): Test
(title: string, fn?: AsyncFunc): Test
only: ExclusiveTestFunction
skip: PendingTestFunction
retries(n: number): void
}
虽然您可以通过两者实现这一点,但在这种情况下我建议坚持使用接口,因为它具有更清晰的语义和更清晰的语法。
合并
✔ 同等
将不同类型的属性合并为一个,使用类型别名时称为交集,使用接口时称为扩展。
type SuperUser = User & { super: true }
// ...
interface SuperUser extends User {
super: true
}
type Z = A & B & C & D & E
// ...
interface Z extends A, B, C, D, E {}
这里有一个显著的区别,仅从这些例子中看不出来。扩展接口时,必须使用扩展结果声明一个新的接口,而使用类型别名,可以内联交集类型,例如:
function(_: A & B) {}
//...
interface Z extends A, B {}
function(_: Z) {}
类实现
✔ 等效(!)
这看起来可能有悖常理,但您可以在类中实现类型别名和接口!
type AnimalType = {}
interface IAnimal = {}
class Dog implements AnimalType {} // ✔ Works
class Cat implements IAnimal {} // ✔ Works
然而,尽管两者都有可能,但由于经典的面向对象语言设计,这种用例通常归因于接口,并且可以肯定地说,您很少会在现实世界的代码库中看到以这种方式使用的类型。
联合类型
❌ 不等同
使用联合类型语法将类型声明为类型别名时,可以定义一个类型,可以是这个类型,也可以是另一个类型,但对于接口来说这是不可能的:
type Z = A | B
//...
interface IZ extends A | B {} // <- ❌ INVALID SYNTAX, not possible to achieve this
也不可能从声明为联合类型的类型进行扩展。
type Z = A | B
interface IZ extends Z {} // ❌ Compilation error:
// "An interface can only extend an object type or intersection
// of object types with statically known members."
重新声明
❌ 不等同
还有另一种扩展接口定义的方法。通过重新声明,最新声明中定义的所有内容都将与所有先前声明的属性合并。因此,可以说接口的行为与 CSS 的层叠特性非常相似。
interface User {
name: string
}
interface User {
gender: string
}
const user: User = { name: 'Ronald', gender: 'male' }
但是使用类型别名无法实现这一点:
type User = { name: string }
type User = { gender: string } // ❌ Compilation error
// "Duplicate identifier 'User'."
如果您需要扩展现有对象的定义(该对象的类型声明超出您的能力范围,即它来自第三方包,或者它是标准库的一部分),这将特别有用。
假设你的 Web 应用向对象添加了几个属性window
。你将无法使用添加的属性,因为它们不属于该Window
类型的原始定义,否则会引发编译错误。但是,由于Window
它被声明为 interface,你可以在客户端应用的入口点附近执行此操作:
declare global {
interface Window {
$: jQuery
}
}
// ...
// and now you use $ globally without type errors
window.$; // 👍
注意:这并不鼓励使用 jQuery。
在 React props 中使用
考虑到所有这些因素,你认为哪一个是定义 React 组件 props 类型的最佳选择?是否存在独特的最佳实践?我们能否说使用其中一种是反模式或应该避免?让我们来一一解答。
当我看到使用接口声明的 props 时,我会立即停下脚步并思考:“将它声明为接口是因为开发人员稍后会在类中实现它吗?”,“将它声明为接口是因为开发人员稍后会重新声明它,还是重新声明的可能性是这个组件的预期功能?如果是,这会对组件的使用产生什么影响?”
然后,我开始寻找这些问题的答案,然后再继续我正在做的事情,大多数时候都没有取得什么成果,因为这些都不是决定使用界面的因素,但此时,我已经浪费了开发时间,更重要的是浪费了宝贵的稀缺认知资源,我永远无法挽回。
不过,当我看到类型别名时,我不会问自己这些问题。类型别名感觉上更像是一种更合适的语言结构,用于清晰地定义对象的形状,并且更类似于函数式编程,因此考虑到 React 本身就是一个用函数式设计用户界面的工具,它更适合 React。另一方面,接口有很多面向对象的包袱,这些包袱在我们专门讨论 React 组件 props 时是无关紧要的,而且面向对象编程也不是 React 的范式。
另外,正如您从前面的示例中所看到的,由于语法的缘故,类型声明几乎总是比接口声明更简洁,而且由于可以使用联合,类型声明也更容易组合。如果您输入的 prop 对象非常小,您也可以在函数声明中将其内联,而如果您严格遵循接口声明,则无法做到这一点。
太棒了,这是否意味着我会一直使用类型来处理 props,而不是接口?如果你去研究一下最流行的 React 可复用组件库的类型定义文件,你会注意到它们大多数都使用接口来处理 props,所以你可以得出结论,出于某种原因,这是全球社区普遍接受的做法。
对于可复用的库来说,使用接口是一个非常实用的选择,因为它可以让库本身更加灵活,因为现在每个使用者都可以根据需要重新声明每个接口来添加属性。这很有用,因为许多开源库的类型定义与其源代码分开维护,因此这些定义过时是很常见的。当它们过时时,用户可以通过利用接口轻松解决这个问题,而维护者自己也不会被社区中大量与编译相关的问题报告所困扰。
但现在让我们设想一个不同的场景。假设你在一家多团队公司工作,许多不同的团队在各自的前端应用中独立工作,但都依赖于一个私有/内部可复用组件库。这个组件库由你的团队拥有,但其他所有人都参与贡献。人类的天性是,总是会努力寻找阻力最小的路径来实现目标。如果你因为上述原因决定使用接口,那么当另一个团队遇到类型不一致的问题时,他们很可能会选择利用扩展点的灵活性,在自己的代码库中快速修复,而不是向上游贡献修复方案,从而进一步破坏整个公司开发体验的一致性。
在这种情况下,我想避免提供过多的扩展或灵活性,并且接口的特性将是有害的。
结论
那么,我的最终答案是什么?类型别名还是接口?我的答案是:“我不在乎”和“视情况而定”。
这两种类型和接口几乎相同,它们的根本区别对于 React 组件 props 这种极其简单的情况来说并不重要。除非有特定的理由(例如我上面列出的示例),否则请使用你觉得合适的任何一种。
我唯一的要求是,不要误导他人,让他们误以为“React props 应该始终使用类型”或“React props 应该始终使用接口声明”,或者认为其中一种是“最佳实践”或“反模式”。所有“最佳实践”之所以是最佳实践,都是因为各种原因,这些原因与具体情况和条件有关,并且可能并非适用于所有情况。根据我的经验,许多工程师没有足够的勇气或信心去挑战这些假设,而是会继续过着可能影响他们职业生涯的谎言生活。
如果你从这篇博文中学到什么的话,那就是:
- 始终挑战先入为主的观念、假设和既定的“最佳实践”。
- 不要忘记最佳实践背后的原因。如果忘记了,在运用它进行论证或基于它做出决策之前,务必先查阅相关资料。
- 如果众多选项之间的界限过于模糊,涉及的因素难以辨别,或者非常琐碎,那就不要浪费你大脑的青春,选择其中一个吧。