6个编写整洁代码的TypeScript技巧

974 阅读5分钟

1. 避免使用非严格模式

不良习惯示例

tsconfig.json中不启用严格模式:

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

推荐做法

启用严格模式:

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

原因分析

在现有代码库中引入更严格规则需花费时间。

弊端说明

更严格规则利于未来代码变更,修复代码的时间投入将有回报,后续处理代码库时也更有益。

2. 摒弃用||定义默认值

不良习惯示例

使用||为可选值提供回退:

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

推荐做法

使用新的??运算符,或在参数级别定义回退值。

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

原因分析

??运算符去年才引入,在长函数中使用值时,难将其设为参数默认值。

弊端说明

??仅在值为nullundefined时回退,与||不同。若函数过长,可考虑拆分并在开头定义默认值。

3. 勿用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方便,基本禁用所有类型检查,官方类型定义中也常用(如response.json()被定义为Promise<any>)。

弊端说明

使用any会跳过所有类型检查,导致难以捕获的错误,只有类型结构假设与运行时代码相关时才会出错。

4. 减少强制类型断言val 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时,现有代码库常对类型做假设,无法自动推断,用as SomeOtherType可加快转换,无需放宽tsconfig设置。

弊端说明

即使断言当前正确,移动代码时可能改变。类型守卫确保检查显式。

5. 避免在测试中使用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);
});

推荐做法

如需模拟测试数据,将模拟逻辑移到模拟对象旁并使其可重用。

interface User {
    id: string;
    firstName: string;
    lastName: string;
    email: string;
}

class MockUser implements User {
    id = 'id';
    firstName = 'John';
    lastName = 'Doe';
    email = 'john@doe.com';
}

test('createEmailText returns text that greats the user by first name', () => {
    const user = new MockUser();
    expect(createEmailText(user)).toContain(user.firstName);
});

原因分析

在测试覆盖率低的代码库中编写测试时,常遇复杂大数据结构,对特定功能仅需部分属性,短期忽略其他属性更轻松。

弊端说明

不创建模拟对象后续会有麻烦,如属性变化需在多处更改。被测试代码可能依赖之前不重要的属性,导致所有相关测试需更新。

6. 优化可选属性使用

不良习惯示例

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

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;
}

原因分析

标记属性为可选更简单,代码量少,需对产品有深入理解,产品假设变化时可能限制代码使用。

弊端说明

类型系统优势在于用编译时检查替代运行时检查。更明确的类型定义可在编译时发现易忽略错误,如确保DigitalProductsizeInMb属性。

7. 避免单字母泛型命名

不良习惯示例

用单字母命名泛型:

function head<T>(arr: T[]): T | undefined {
    return arr[0];

推荐做法

使用完整描述性类型名称。

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

原因分析

官方文档使用单字母名称,输入快,按单字母比写完整名称思考少。

弊端说明

泛型类型变量同其他变量,单字母变量名难理解,放弃了用变量名描述技术细节的机会,不利于代码阅读和理解。

8. 避免非布尔值的布尔检查

不良习惯示例

通过将值直接传给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';

原因分析

简短编写检查看似简洁,避免思考实际检查内容。

弊端说明

应思考实际检查内容,如上述示例对countOfNewMessages0的处理不同。

9. 不使用双感叹号运算符转换布尔值

不良习惯示例

将非布尔值转换为布尔值:

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';

原因分析

对部分人而言,!!像是JavaScript入门标志,简短简洁,若习惯则知其用途,是转换布尔值的快捷方式,在代码库中假值语义区分不明确时常用。

弊端说明

使用!!掩盖代码真实含义,对新开发者难理解,易引入微妙错误,如countOfNewMessages0时的问题仍存在。

10. 不使用!= null同时检查nullundefined

不良习惯示例

!= null可同时检查nullundefined

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';

原因分析

多数linting规则集对!= null做例外处理,若代码库中nullundefined无明确区别,!= null可缩短检查。

弊端说明

在TypeScript严格模式下,null可成为有价值的语言工具,如可定义user.firstName === null表示用户无名字,user.firstName === undefined表示未询问过用户,user.firstName === ''表示名字为空字符串,!= null模糊了这种区分。