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

web前端开发

共 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 旨在帮助你构建更好的代码库,而不是妨碍你。

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

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

感谢你的阅读。

学习更多技能

请点击下方公众号

浏览 9
点赞
评论
收藏
分享

手机扫一扫分享

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

手机扫一扫分享

分享
举报