7个Typescript常见错误需要你注意避免发生

共 7610字,需浏览 16分钟

 ·

2021-08-10 14:43

英文 | 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 finelogNumber.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 laxconsole.log(x.name.toLowerCase());

// ✅ Fails with strictNullChecks on as x.name could be undefinedconsole.log(x.name.toLowerCase());

strictFunctionTypes:启用此标志可确保更彻底地检查函数参数。

strictPropertyInitialization:当设置为 true 时,这将强制我们在构造函数中设置所有属性值。

正如所见,TypeScript 的严格变量是上述所有标志的简写。我们可以通过使用严格或逐步启用它们来启用它们。

更严格的类型将帮助我们在编译时捕获更多错误。

2、重新声明接口

在键入组件接口时,通常需要具有相同类型的一些不同接口变体。这些可以在一两个参数中变化。一个常见的错误是手动重新定义这些变体。这将导致:

  • 不必要的样板。

  • 需要多次更改。如果一个属性在一个地方发生变化,则需要将该更改传播到多个文件。

很久以前,TypeScript 发布了一个旨在解决此目的的功能:映射类型。它们让我们可以根据我们定义的一些规则,在现有类型的基础上创建新类型。这确实会导致更具可读性和声明性的代码库。

让我们看一个例子:

interface Book {  author?: string;  numPages: number;  price: number;}
// ✅ Article is a Book without a Pagetype Article = Omit<Book, 'numPages'>;
// ✅ We might need a readonly verison of the Book Typetype ReadonlyBook = Readonly<Book>;
// ✅ A Book that must have an authortype 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 returnsconst 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 itconst 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 itinterface 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 inferenceconst 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 numbertype X = typeof array extends (infer U)[] ? U : never;

在上面的例子中,我们可以看到如何提取数组的类型。

4、不正确的使用 Overloading

TypeScript 本身支持重载。这很好,因为它可以提高我们的可读性。但是,它不同于其他类型的重载语言。

在某些情况下,它可能会使我们的代码更加复杂和冗长。为了防止这种情况发生,我们需要牢记两条规则:

1. 避免编写多个仅尾随参数不同的重载

// ❌ instead of thisinterface Example {  foo(one: number): number;  foo(one: number, two: number): number;  foo(one: number, two: number, three: number): number;}
// ❎ do thisinterface Example { foo(one?: number, two?: number, three?: number): number;}

你可以看到两个接口是如何相等的,但第一个比第二个更冗长。在这种情况下最好使用可选参数。

2. 避免仅在一种参数类型中编写因类型不同而不同的重载

// ❌ instead of thisinterface Example {  foo(one: number): number;  foo(one: number | string): number;}
// ❎ do thisinterface Example { foo(one: number | string): number;}

与前面的示例一样,第一个界面变得非常冗长。最好使用联合来代替。

5、使用函数类型

TypeScript 附带 Function 类型。这就像使用 any 关键字但仅用于函数。遗憾的是,启用严格模式不会阻止我们使用它。

这里有一点关于函数类型:

  • 它接受任意数量和类型的参数。

  • 返回类型始终为 any。

让我们看一个例子:

// ❌ Avoid, parameters types and length are unknown. Return type is anyconst onSubmit = (callback: Function) => callback(1, 2, 3);
// ✅ Preferred, the arguments and return type of callback is now clearconst onSubmit = (callback: () => Promise<unknown>) => callback();

在上面的代码中,通过使用显式函数定义,我们的回调函数更具可读性和类型安全性。

6、依赖第三方实现不变性

在使用函数式编程范式时,TypeScript 可以提供很大帮助。它提供了所有必要的工具来确保我们不会改变我们的对象。我们不需要在我们的代码库中添加像 ImmutableJS这样的笨重的库。

让我们通过以下示例来看看我们可以使用的一些工具:

// ✅ declare properties as readonlyinterface Person {  readonly name: string;  readonly age: number;}
// ✅ implicitely declaring a readonly arraysconst x = [1,2,3,4,5] as const;
// ✅ explicitely declaring a readonly arrayconst y: ReadonlyArray<{ x: number, y: number}> = [ {x: 1, y: 1}]
interface Address { street: string; city: string;}
// ✅ converting all the type properties to readonlytype 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 assignableconst fails: StringHttpResponse = { data: 'test', included: {} // ^^^^^ // Type '{}' is not assignable to type 'never'}
// ✅ included is not assignedconst works: StringHttpResponse = { data: 'test',}

在上面的代码中,我们可以使用 never 类型来表示我们不希望某个属性是可赋值的。

我们可以将 Omit Mapped 类型用于相同的目的:

type StringHttpResponse = Omit<HttpResponse<string, unkown>, 'included'>;

但是,你可以看到它的缺点。它更冗长。如果你检查 Omit 的内部结构,它会使用 Exclude,而后者又使用 never 类型。

通过依赖 infer 和 never 关键字,我们省去了复制任何类型的麻烦,并更好地表达我们的接口。

总结

这些指南易于遵循,旨在帮助你接受 TypeScript,而不是与之抗争。TypeScript 旨在帮助你构建更好的代码库,而不是妨碍你。

通过应用这些简单的技巧,你将拥有一个更好、更简洁且易于维护的代码库。

我们是否遗漏了你项目中经常发生的任何常见错误?请在评论中与我分享它们。

感谢你的阅读。

学习更多技能

请点击下方公众号

浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报