vlambda博客
学习文章列表

打破 10 个 Typescript 编程坏习惯

【CSDN 编者按】好习惯受益终生,坏习惯尽快改掉!


作者 | Daniel Bartholomae   译者 | 明明如月
出品 | CSDN(ID:CSDNnews)
在过去的几年间,TypeScript 和 JavaScript 得到了稳步发展,与此同时,很多开发者也养成了很多已经过时的编程习惯。其中一些过时的编程习惯已经没啥意义。
下文将列举出我们应该破除的 10 个编程习惯。 需要注意的是,每个“该怎么怎么写” 只修复讨论的这个话题,并不代表此代码没有任何其他“坏味道”。


打破 10 个 Typescript 编程坏习惯

不使用严格模式


例子:不使用严格模式的 tsconfig.json

 
   
   
 
{ "compilerOptions": { "target": "ES2015", "module": "commonjs" }}

应该怎样写?启用严格模式

 
   
   
 
{ "compilerOptions": { "target": "ES2015", "module": "commonjs", "strict": true }}

为什么有些人会那么写?在现有代码库中引入更严格的规则需要很大的时间成本。

为什么不应该这么做?更严格的规则将使得未来修改代码时更加容易,因此可以减少我们修复代码所花费的时间。


打破 10 个 Typescript 编程坏习惯

使用 || 定义默认值


例子:通过 || 填充可选值

 
   
   
 
function createBlogPost (text: string, author: string, date?: Date) { return { text: text, author: author, date: date || new Date() }}

应该怎样写?推荐使用 ?? 运算符,或者定义参数级 fallback。

 
   
   
 
function createBlogPost (text: string, author: string, date: Date = new Date()) return { text: text, author: author, date: date }}

为什么有些人会那么写?

?? 操作符在去年才被引入,所以引入之前很多人会使用 || 来设置默认值。当在长函数中间使用值时,可能很难将它们设置为参数默认值。

为什么不应该这么做?

?? 不同于 || ,不会对所有的假值,而只对 null 或 undefined 的值起作用。
此外,如果函数太长,以至于无法在开始时定义默认值,那么将它们分隔开就是一个明智的选择。


打破 10 个 Typescript 编程坏习惯

使用 any 作为类型


例子:当你不确定数据结构时,使用了 any 当做数据类型

 
   
   
 
async function loadProducts(): Promise<Product[]> { const response = await fetch('https://api.mysite.com/products') const products: any = await response.json() return products}

应该怎么写?每当你有想输入 any 的欲望时,都你应该使用unknown` 代替。

 
   
   
 
async function loadProducts(): Promise<Product[]> { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() return products as Product[]}

为什么有些人会那么写?any 写起来很方便,因为它基本上禁用所有类型检查。通常,甚至在正式输入中也会使用any (例如,上面示例中的 response.json() 被 TypeScript 团队定义为 Promise<any>)。

为什么不应该那么写?这样写基本上禁用所有类型检查。任何 any 类型的类型检查都将被跳过。这样,当类型结构的推断与运行期不一致时,代码错误才会保留出来,很容易引发一些不易发现的 bug。


打破 10 个 Typescript 编程坏习惯

as SomeType 类型断言


例子:有力地告诉编译器不能推断的类型。

 
   
   
 
async function loadProducts(): Promise<Product[]> { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() return products as Product[]}

应该怎么写?

 
   
   
 
function isArrayOfProducts (obj: unknown): obj is Product[] { return Array.isArray(obj) && obj.every(isProduct)}
function isProduct (obj: unknown): obj is Product { return obj != null && typeof (obj as Product).id === 'string'}
async function loadProducts(): Promise<Product[]> { const response = await fetch('https://api.mysite.com/products') const products: unknown = await response.json() if (!isArrayOfProducts(products)) { throw new TypeError('Received malformed products API response') } return products}

为什么有些人会那么写?

当从 JavaScript 转换到 TypeScript 时,现有的代码库通常会对无法由 TypeScript 编译器自动推导的类型进行推断。在这种情况下,添加一个类型为 as SomeOtherType 的快速设置可以加快转换速度,而不必散落在 ts 配置中进行设置。

为什么不应该那么写?

即使现在代码中加入了断言,也不保证未来有谁会修改这些代码。类型保护能确保显式类型检查。


打破 10 个 Typescript 编程坏习惯

在测试中使用 as any


例子:在编写测试时创建不完整的替代代码。

 
   
   
 
interface User { id: string firstName: string lastName: string email: string}
test('createEmailText returns text that greats the user by first name', () => { const user: User = { firstName: 'John' } as any expect(createEmailText(user)).toContain(user.firstName)}

应该怎么写?如果你需要为测试而 mock 数据,那就将 mock 逻辑和 mock 的对象放在一起,并使其可重用。

 
   
   
 
interface User { id: string firstName: string lastName: string email: string}
class MockUser implements User { id = 'id' firstName = 'John' lastName = 'Doe' email = '[email protected]'}
test('createEmailText returns text that greats the user by first name', () => { const user = new MockUser()
expect(createEmailText(user)).toContain(user.firstName)}

为什么有些人会那么写?

在一个还没有很好的测试覆盖率的代码库中编写测试时,通常会有复杂的大数据结构,但是只有部分需要用于测试的特定功能。在短期内,不担心其他属性,这样做会更简单一些。

为什么不应该那么写?

如果其中一个属性发生了变化,如果没有将 mock 逻辑和对象放在一起并且不可重用,那么我们每个地方都要修改,让我们非常头疼。此外,还会出现这样的情况,即被测试的代码依赖于我们以前认为不重要的属性,然后需要修改该功能的所有测试方法。


打破 10 个 Typescript 编程坏习惯

可选属性


例子:将有时存在有时不存在的属性标记为可选:

 
   
   
 
interface Product { id: string type: 'digital' | 'physical' weightInKg?: number sizeInMb?: number}

应该怎么写?显式地进行属性组合

 
   
   
 
interface Product { id: string type: 'digital' | 'physical'}
interface DigitalProduct extends Product { type: 'digital' sizeInMb: number}
interface PhysicalProduct extends Product { type: 'physical' weightInKg: number}

为什么有些人会那么写?

将属性标记为可选类型而不是分割出类型写起来更容易,并且代码量更少。它还需要对正在构建的产品有更深入的理解,并且如果对产品的假设发生变化,可能会限制代码的使用。

为什么不应该那么写?

类型系统的最大好处是,它们可以用编译时检查代替运行时检查。通过更明确的输入,可以在编译时检查错误,否则可能会被忽视。上面的例子可以确保每个 DigitalProduct 都有 sizeInMb 属性。


打破 10 个 Typescript 编程坏习惯

只用一个字母描述泛型


仅用一个字母对泛型进行描述
 
   
   
 
function head<T> (arr: T[]): T | undefined { return arr[0]}

应该怎么写?给出完整的描述性类型名称

 
   
   
 
function head<Element> (arr: Element[]): Element | undefined { return arr[0]}

为什么有些人会那么写?

我猜这个习惯的形成是因为连官方文件也存在使用单字母作为泛型名字的现象。这样做输入速度更快,而且不需要太多思考,只要按下 t 键,而不需要写下全名。

为什么不应该那么写?

像任何其他变量一样,泛型类型变量也是一种变量。当 IDE 开始向我们展示这些技术细节时,我们已经放弃了在名称中描述变量的技术细节的想法,如我们现在可以写成: const name = 'Daniel'而不必要写成: const strName = 'Daniel'这种格式。此外,用一个字母作为变量名难以理解,在不看它的类型声明之前很难搞懂它的含义。


打破 10 个 Typescript 编程坏习惯

非布尔类型用作布尔检查


例子:通过将值直接传递给 if 语句来检查是否定义了值。

 
   
   
 
function createNewMessagesResponse (countOfNewMessages?: number) { if (countOfNewMessages) { return `You have ${countOfNewMessages} new messages` } return 'Error: Could not retrieve number of new messages'}

应该怎么写?显式地检查我们关心的条件。

 
   
   
 
function createNewMessagesResponse (countOfNewMessages?: number) { if (countOfNewMessages !== undefined) { return `You have ${countOfNewMessages} new messages` } return 'Error: Could not retrieve number of new messages'}

为什么有些人会那么写?

那样写起来更简洁,但容易让我们忽略我们真正要检查的是啥。

为什么不应该那么写?

也许我们应该考虑一下我们真正想要检查的是什么。例如, countOfNewMessages 为 0 时上述代码会有差异。


打破 10 个 Typescript 编程坏习惯

!! 操作符


例子:将非布尔值转换为布尔值。

 
   
   
 
function createNewMessagesResponse (countOfNewMessages?: number) { if (!!countOfNewMessages) { return `You have ${countOfNewMessages} new messages` } return 'Error: Could not retrieve number of new messages'}

应该怎么写?显式地检查我们关心的条件。

 
   
   
 
function createNewMessagesResponse (countOfNewMessages?: number) { if (countOfNewMessages !== undefined) { return `You have ${countOfNewMessages} new messages` } return 'Error: Could not retrieve number of new messages'}

为什么有些人会那么写?

对于一些人来说,理解 !! 操作符非常容易。它可以将任何值转为布尔类型。特别在代码库中假值(如 null、undefined 和 '')之间没有明确的语义区分时,这样写更加简洁。

为什么不应该那么写?

!! 用起来非常简洁,但是它会模糊代码的真正含义,这对新手来说非常不友好,还容易引起一些不易察觉的错误。
当 countOfNewMessages 的值为 0 时,使用“!! 操作符”和“使用非布尔类型用作布尔检查”两种现象都存在。


打破 10 个 Typescript 编程坏习惯

!=null


例子:!=null 可以同时检查 null 和 undefined

 
   
   
 
function createNewMessagesResponse (countOfNewMessages?: number) { if (countOfNewMessages != null) { return `You have ${countOfNewMessages} new messages` } return 'Error: Could not retrieve number of new messages'}

应该怎么写?显式地检查我们关心的条件。

 
   
   
 
function createNewMessagesResponse (countOfNewMessages?: number) { if (countOfNewMessages !== undefined) { return `You have ${countOfNewMessages} new messages` } return 'Error: Could not retrieve number of new messages'}

为什么有些人会那么写?

如果代码库中对 null 和 undefined 不进行严格的区分,那么使用 !=null 将让代码更加简洁。

为什么不应该那么写?

在 JavaScript 产生之初处理 null 值相当麻烦,而在 TypeScript 的严格模式下,它们的价值就凸显了出来。通常在某个事物不存在时可以使用 null表示 ,而在未定义时使用 undefined。user.firstName === null 表示用户没有名字。user.firstName === undefined 意味着我们尚未询问用户。而 user.firstName === ‘’`` 却意味着名字就是空字符串。
以上。
原文链接:https://startup-cto.net/10-bad-typescript-habits-to-break-this-year/
打破 10 个 Typescript 编程坏习惯

打破 10 个 Typescript 编程坏习惯