英文 | https://betterprogramming.pub/7-typescript-common-mistakes-to-avoid-581c30e514d6
翻译 | 杨小二
自 2012 年 10 月首次出现以来,TypeScript 获得了广泛的关注,它已成为 Web 开发领域真正的游戏规则改变者。尽管如此,有些人一直对使用它持怀疑态度。
将 TypeScript 添加到项目时,开发人员应该接受它而不是反对它,这一点很重要。
这可能会使你沮丧,以及在开发中TypeScript 会成为开发过程中的瓶颈。
但是,如果使用得当,拥有可读且易于维护的代码库就变得至关重要。
它具有强大的功能,例如映射类型、重载、类型推断、可选类型等,并且随着增量升级,这些功能每天都在变得更好。
为什么有些人会觉得 TypeScript 正在损害他们的生产力?我们怎样才能防止这种不好情况的发生?是否有一些我们可以采用的最佳实践做法?
在这里,我们将讨论使用 TypeScript 时最常见的错误。通过不迷恋这些常见的问题,我们将看到我们的生产力和代码可维护性的提高。
现在开始吧。
1、不启用严格模式
如果没有打开 TypeScript 严格模式,类型可能会过于松散,这将使我们的代码库类型安全性降低。它会给人错误的印象,因为有些人认为通过添加 TypeScript,所有TypeScript问题都会自动修复。
以后,我们将成为这些类型的受害者。我们最终可能会用补丁修复它们,而不是修复根本原因。这可能会导致你认为该工具做得不好。
我们如何启用严格模式?它通过在 tsconfig.json 文件中将 strict 参数设置为 true 来启用,如下所示:
{
...
"compilerOptions": {
"strict": true,
...
},
...
}
启用strict模式将在钩子下启用:
noImplicitAny:此标志可防止我们使用推断的 any 公开合约。如果我们不指定类型并且无法推断,则默认为any。
noImplicitThis:它将防止 this 关键字的不必要的不安全用法。防止不需要的行为将使我们免于一些调试麻烦,如下所示:
class Book {
pages: number;
constructor(totalPages: number) {
this.pages = totalPages;
}
isLastPageFunction() {
return function (currentPage: number) {
// ❌ 'this' here implicitly has type 'any' because it does not have a type annotation.
return this.pages === currentPage;
}
}
}
alwaysStrict:这将确保在我们所有转换后的 JavaScript 文件中发出 use strict ,但编译器除外。这将提示 JavaScript 引擎代码应该在严格模式下执行。
strictBindCallApply:这将确保我们使用正确的参数调用 call 、 bind 和 apply 函数。让我们看一个例子:
const logNumber = (x: number) => {
console.log(`number ${x} logged!`)
}
// ✅ works fine
logNumber.call(undefined, 10);
// ❌ error: Argument of type 'string' is not assignable to parameter of type 'number'.ts(2345)
logNumber.call(undefined, "10");
strictNullChecks:如果此标志关闭,则编译器会有效地忽略 undefined、null 和 false。松散的输入可能会导致运行时出现意外错误。让我们看一个例子:
interface Person {
name: string | undefined;
age: number;
}
const x: Person = { name: 'Max', age: 3 };
// ❌ Works with strictNullChecks off, which is lax
console.log(x.name.toLowerCase());
// ✅ Fails with strictNullChecks on as x.name could be undefined
console.log(x.name.toLowerCase());
strictFunctionTypes:启用此标志可确保更彻底地检查函数参数。
strictPropertyInitialization:当设置为 true 时,这将强制我们在构造函数中设置所有属性值。
正如所见,TypeScript 的严格变量是上述所有标志的简写。我们可以通过使用严格或逐步启用它们来启用它们。
更严格的类型将帮助我们在编译时捕获更多错误。
2、重新声明接口
在键入组件接口时,通常需要具有相同类型的一些不同接口变体。这些可以在一两个参数中变化。一个常见的错误是手动重新定义这些变体。这将导致:
不必要的样板。
需要多次更改。如果一个属性在一个地方发生变化,则需要将该更改传播到多个文件。
很久以前,TypeScript 发布了一个旨在解决此目的的功能:映射类型。它们让我们可以根据我们定义的一些规则,在现有类型的基础上创建新类型。这确实会导致更具可读性和声明性的代码库。
让我们看一个例子:
interface Book {
author?: string;
numPages: number;
price: number;
}
// ✅ Article is a Book without a Page
type Article = Omit<Book, 'numPages'>;
// ✅ We might need a readonly verison of the Book Type
type ReadonlyBook = Readonly<Book>;
// ✅ A Book that must have an author
type NonAnonymousBook = Omit<Book, 'author'> & Required<Pick<Book, 'author'>>;
在上面的代码中,我们保留了一个单一的事实来源:Book 实体。它的所有变体都使用映射类型功能来表达,这大大减少了对代码进行类型化和维护的成本。
映射类型也可以应用于联合,如下所示:
type animals = 'bird' | 'cat' | 'crocodile';
type mamals = Exclude<animals, 'crocodile'>;
// 'bird' | 'cat'
TypeScript 附带以下映射类型:Omit、Partial、Readonly、Exclude、Extract、NonNullable、ReturnType。
我们可以创建自己的实用程序并在我们的代码库中重用它们。
3、不依赖类型推断
TypeScript 推理是这种编程语言最强大的工具之一。它为我们完成所有工作。我们只需要确保在尽可能少的干预下将所有部分加在一起。
实现这一目标的一个关键操作符是 typeof。它是一个类似于 JavaScript 的运算符。它不会返回 JavaScript 类型,而是返回 TypeScript 类型。使用这个操作数可以避免我们重新声明相同的类型。
让我们通过一个例子来看看:
const addNumber = (a: number, b: number) => a + b;
// ❌ you are hardcoding the type `number` instead of relying on what the function returns
const processResult = (result: number) => console.log(result);
processResult(addNumber(1, 1));
// ✅ result will be whatever the return type of addNumber function
// no need for us to redefine it
const processResult = (result: ReturnType<typeof addNumber>) => console.log(result);
processResult(addNumber(1, 1));
在上面的代码中,注意结果参数类型。最好依赖 ReturnType<typeof addNumber> 而不是添加数字类型。通过对数字类型进行硬编码,我们完成了编译器的工作。
最好使用适当的语法来表达我们的类型。TypeScript 将为我们完成繁重的工作。
让我们看一个虚拟示例:
// ❌ Sometimes for one of objects it is not necessary to define
// interfaces for it
interface Book {
name: string,
author: string
}
const book: Book = {
name: 'For whom the bell tolls',
author: 'Hemingway'
}
const printBook = (bookInstance: Book) => console.log(bookInstance)
请注意,Book 接口用于特定场景,甚至不需要创建接口。
通过依赖 TypeScript 的推理,代码变得不那么杂乱,更易于阅读。下面是一个例子:
// ✅ For simple scenarios we can rely on type inference
const book = {
name: 'For whom the bell tolls',
author: 'Hemingway'
}
const printBook = (bookInstance: typeof book) => console.log(bookInstance)
TypeScript 甚至有 infer 运算符,它可以与 Mapped Types 结合使用以从另一个类型中提取一个类型。
const array: number[] = [1,2,3,4];
// ✅ type X will be number
type X = typeof array extends (infer U)[] ? U : never;
在上面的例子中,我们可以看到如何提取数组的类型。
4、不正确的使用 Overloading
TypeScript 本身支持重载。这很好,因为它可以提高我们的可读性。但是,它不同于其他类型的重载语言。
在某些情况下,它可能会使我们的代码更加复杂和冗长。为了防止这种情况发生,我们需要牢记两条规则:
1. 避免编写多个仅尾随参数不同的重载
// ❌ instead of this
interface Example {
foo(one: number): number;
foo(one: number, two: number): number;
foo(one: number, two: number, three: number): number;
}
// ❎ do this
interface Example {
foo(one?: number, two?: number, three?: number): number;
}
你可以看到两个接口是如何相等的,但第一个比第二个更冗长。在这种情况下最好使用可选参数。
2. 避免仅在一种参数类型中编写因类型不同而不同的重载
// ❌ instead of this
interface Example {
foo(one: number): number;
foo(one: number | string): number;
}
// ❎ do this
interface Example {
foo(one: number | string): number;
}
与前面的示例一样,第一个界面变得非常冗长。最好使用联合来代替。
5、使用函数类型
TypeScript 附带 Function 类型。这就像使用 any 关键字但仅用于函数。遗憾的是,启用严格模式不会阻止我们使用它。
这里有一点关于函数类型:
它接受任意数量和类型的参数。
返回类型始终为 any。
让我们看一个例子:
// ❌ Avoid, parameters types and length are unknown. Return type is any
const onSubmit = (callback: Function) => callback(1, 2, 3);
// ✅ Preferred, the arguments and return type of callback is now clear
const onSubmit = (callback: () => Promise<unknown>) => callback();
在上面的代码中,通过使用显式函数定义,我们的回调函数更具可读性和类型安全性。
6、依赖第三方实现不变性
在使用函数式编程范式时,TypeScript 可以提供很大帮助。它提供了所有必要的工具来确保我们不会改变我们的对象。我们不需要在我们的代码库中添加像 ImmutableJS这样的笨重的库。
让我们通过以下示例来看看我们可以使用的一些工具:
// ✅ declare properties as readonly
interface Person {
readonly name: string;
readonly age: number;
}
// ✅ implicitely declaring a readonly arrays
const x = [1,2,3,4,5] as const;
// ✅ explicitely declaring a readonly array
const y: ReadonlyArray<{ x: number, y: number}> = [ {x: 1, y: 1}]
interface Address {
street: string;
city: string;
}
// ✅ converting all the type properties to readonly
type ReadonlyAddress = Readonly<Address>;
正如你从上面的例子中看到的,我们有很多工具来保护我们的对象免于变异。
通过使用内置功能,我们将保持我们的 bundle light 和我们的类型一致。
7、不理解 infer/never 关键字
infer 和 never 关键字很方便,可以在许多情况下提供帮助,例如:
推断
使用 infer 关键字就像告诉 TypeScript,“我想把你在这个位置推断出来的任何东西分配给一个新的类型变量。”
我们来看一个例子:
const array: number[] = [1,2,3,4];
type X = typeof array extends (infer U)[] ? U : never;
在上面的代码中,作为array extends infer U[],X变量将等于 a Number。
never
该never类型表示值是不会发生的类型。
我们来看一个例子:
interface HttpResponse<T, V> {
data: T;
included?: V;
}
type StringHttpResponse = HttpResponse<string, never>;
// ❌ included prop is not assignable
const fails: StringHttpResponse = {
data: 'test',
included: {}
// ^^^^^
// Type '{}' is not assignable to type 'never'
}
// ✅ included is not assigned
const works: StringHttpResponse = {
data: 'test',
}
在上面的代码中,我们可以使用 never 类型来表示我们不希望某个属性是可赋值的。
我们可以将 Omit Mapped 类型用于相同的目的:
type StringHttpResponse = Omit<HttpResponse<string, unkown>, 'included'>;
但是,你可以看到它的缺点。它更冗长。如果你检查 Omit 的内部结构,它会使用 Exclude,而后者又使用 never 类型。
通过依赖 infer 和 never 关键字,我们省去了复制任何类型的麻烦,并更好地表达我们的接口。
总结
这些指南易于遵循,旨在帮助你接受 TypeScript,而不是与之抗争。TypeScript 旨在帮助你构建更好的代码库,而不是妨碍你。
通过应用这些简单的技巧,你将拥有一个更好、更简洁且易于维护的代码库。
我们是否遗漏了你项目中经常发生的任何常见错误?请在评论中与我分享它们。
感谢你的阅读。
学习更多技能
请点击下方公众号