TypeScript规范探索

178 阅读8分钟

希望通过本文的介绍和分析,能够让大家对ts类型声明有一个清晰的认识

1. 项目结构

1.1 项目目录结构应具备良好的组织性,便于开发者快速定位和维护代码。

  • 将源代码放在src目录下,测试代码放在tests目录下。
  • src目录下按照功能或模块将代码组织成子目录,例如components用于存放组件代码,services用于存放服务代码,utils用于存放工具函数等。
  • 使用types目录存放类型定义文件。

2. 代码规范

2.1 使用有意义的命名

  • 使用驼峰命名法(camelCase)命名变量、函数和接口。例如:getUserData, formatDate.
  • 使用帕斯卡命名法(PascalCase)命名类和枚举。例如:UserService, UserRole.

2.2 使用明确的类型定义

  • 尽量使用明确的类型定义,避免使用隐式的any类型。
  • 使用接口(interfaces)来定义对象类型,使用类型别名(type aliases)来定义复杂的类型。例如:
interface User {
  id: number;
  name: string;
}

type Point = {
  x: number;
  y: number;
};

2.3 使用强制类型检查

  • 启用 TypeScript 的严格模式(strict mode)来获得更强的类型检查。
  • 避免使用显式的类型断言,除非必要。尽量依赖类型推断。

2.4 使用模块化

  • 使用模块化的方式组织代码,避免全局命名空间的污染。
  • 使用exportimport关键字明确导出和引入模块。

2.5 使用注释

  • 使用注释来解释代码的意图和特殊考虑事项。
  • 使用 JSDoc 风格的注释来提供函数和接口的文档说明。例如:
/**
 * 获取用户数据
 * @param {number} id - 用户ID
 * @returns {Promise<User>} 用户数据
 */
async function getUserData(id: number): Promise<User> {
  // ...
}

2.6 遵循单一职责原则

  • 函数和类应该具备单一职责,避免出现过于庞大的函数或类。
  • 将复杂的逻辑拆分为更小的函数或模块,提高可读性和可维护性。

2.7 异常处理

  • 使用 try-catch 块来捕获和处理异常。
  • 避免在代码中使用空的 catch 块。

3. 代码风格

3.1 缩进和空格

  • 使用两个空格作为缩进。
  • 在运算符前后添加空格,以增加可读性。例如:
const sum = a + b;
const isTrue = (x === y) && (z > 0);

3.2 换行和括号

  • 在语句结束后换行。
  • 在函数和控制流语句的主体周围使用大括号。
  • 在对象和数组字面量的元素之间使用逗号,并在末尾添加逗号。例如:
const users = [
  "Alice",
  "Bob",
  "Charlie",
];
const person = {
  name: "John",
  age: 30,
};

3.3 字符串

  • 对于短字符串,推荐使用单引号。例如:const message = 'Hello, world!'.
  • 对于包含变量或表达式的字符串,使用模板字符串。例如:
    const greeting =  `Hello, ${name}`
    

3.4 函数和方法

  • 使用函数声明或箭头函数来定义函数。
  • 在函数参数列表的括号内使用空格分隔参数。例如:
function greet(name: string, age: number): void {
  // ...
}

const multiply = (a: number, b: number): number => {
  // ...
};

3.5 注释

  • 使用注释来解释代码的意图、算法和特殊情况。
  • 对于复杂的逻辑,添加合适的注释以提高代码的可读性。
  • 在注释前面使用 // 表示单行注释,使用 /* ... */ 表示多行注释。例如:
// 计算两个数的和
function add(a: number, b: number): number {
  return a + b;
}

/*
 * 根据用户的年龄和性别过滤数据
 * @param {number} age - 年龄
 * @param {string} gender - 性别
 * @returns {User[]} 过滤后的用户数组
 */
function filterUsersByAgeAndGender(age: number, gender: string): User[] {
  // ...
}

3.6 其他

  • 遵循一致的命名约定和代码风格。
  • 删除无用的代码和注释,保持代码的整洁性。
  • 使用合适的设计模式和最佳实践来提高代码的可维护性和可测试性。

4.一些补充

当涉及到接口(interfaces)、类型别名(type aliases)和函数类型时,以下是更详细的说明:

4.1 接口(interfaces)

  • 使用接口来定义对象类型,它们描述了对象的结构和属性。
  • 使用关键字interface来定义接口,并遵循帕斯卡命名法(PascalCase)。
  • 接口可以包含属性、方法和索引签名等。

示例:

interface User {
  id: number;
  name: string;
  age: number;
}

interface Book {
  title: string;
  author: string;
}

interface Printable {
  print(): void;
}

4.2 类型别名(type aliases)

  • 使用类型别名来创建自定义类型,可以用来定义复杂的类型或提供更具表达性的名称。
  • 使用关键字type来定义类型别名,并遵循帕斯卡命名法(PascalCase)。

示例:

type UserID = number;
type UserMap = Map<UserID, User>;
type Callback = (result: string) => void;
type Person = { name: string; age: number };

4.3 函数类型

  • 使用类型别名或接口来定义函数类型。
  • 函数类型可以指定参数的类型和返回值的类型。

使用类型别名:

type AddFunction = (a: number, b: number) => number;

const add: AddFunction = (a, b) => a + b;

使用接口:

interface Calculate {
  (a: number, b: number): number;
}

const multiply: Calculate = (a, b) => a * b;

4.4 可选属性和只读属性

  • 接口中的属性可以使用?标记为可选属性,表示可以有或没有该属性。
  • 接口中的属性可以使用readonly关键字标记为只读属性,表示只能在创建时进行赋值。

示例:

interface User {
  id: number;
  name: string;
  age?: number; // 可选属性
  readonly createdAt: Date; // 只读属性
}

4.5 继承和实现

  • 接口可以通过extends关键字来继承其他接口,从而扩展属性和方法。
  • 类可以通过implements关键字来实现接口,从而保证类具有接口中定义的属性和方法。

示例:

interface Animal {
  name: string;
  makeSound(): void;
}

interface Dog extends Animal {
  breed: string;
}

class Labrador implements Dog {
  name: string;
  breed: string;

  makeSound() {
    console.log('Woof!');
  }
}

4.6 Void 类型

  • Void 是 TypeScript 中表示没有返回值的类型。
  • 当函数不返回任何值时,其返回类型可以标记为 void。
  • Void 类型的变量只能赋值为 undefined 或 null。

示例:

function logMessage(message: string): void {
  console.log(message);
}

function doSomething(): void {
  // 执行一些操作,但没有返回值
}

let result: void = undefined;

5. interface 跟 type 的选择

在选择使用接口(interfaces)还是类型别名(type aliases)时,可以考虑以下几个方面:

  1. 对象的形状 vs. 可重用的类型

    • 如果你需要描述一个对象的形状,包括属性、方法和索引签名等,那么使用接口是一个很好的选择。
    • 如果你需要创建可重用的类型,或者需要定义复杂的联合类型、交叉类型等,那么类型别名可以更好地满足这些需求。
  2. 继承 vs. 联合类型

    • 如果你需要在多个接口之间共享属性和方法,以及进行接口的继承和实现,那么使用接口是更合适的。
    • 如果你需要将多个类型组合成一个类型,可以使用联合类型或交叉类型,这时类型别名更适合。
  3. 扩展性 vs. 可读性

    • 接口具有扩展性,可以在现有接口的基础上添加新的属性或方法。
    • 类型别名更注重可读性,可以为复杂的类型定义一个易于理解的名称。

当涉及到举例来说明接口(interfaces)和类型别名(type aliases)的选择时,我们可以考虑以下场景:

假设我们正在开发一个电商网站,需要定义产品的类型。我们可以使用接口和类型别名来描述产品的属性和方法。

使用接口(interfaces)的例子:

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  getImageUrl(): string;
}

interface Book extends Product {
  author: string;
  pages: number;
}

const book: Book = {
  id: 1,
  name: "The Catcher in the Rye",
  price: 10.99,
  description: "A classic novel",
  author: "J.D. Salinger",
  pages: 224,
  getImageUrl() {
    return "https://example.com/book.jpg";
  }
}

在上面的例子中,我们使用接口 Product 来描述产品的通用属性和方法,并使用接口 Book 继承了 Product,并添加了特定于书籍的属性。这种情况下,接口的继承特性非常有用。

使用类型别名(type aliases)的例子:

type Product = {
  id: number;
  name: string;
  price: number;
  description: string;
  getImageUrl(): string;
}

type Book = Product & {
  author: string;
  pages: number;
}

const book: Book = {
  id: 1,
  name: "The Catcher in the Rye",
  price: 10.99,
  description: "A classic novel",
  author: "J.D. Salinger",
  pages: 224,
  getImageUrl() {
    return "https://example.com/book.jpg";
  }
}

在上述例子中,我们使用类型别名 Product 来定义产品的类型,并使用类型别名 Book 来定义书籍的类型。通过使用交叉类型(&),我们将通用的产品类型与特定于书籍的属性组合在一起。

根据上述示例,如果我们只需要描述产品的属性和方法,并且不需要进行继承和实现,那么使用类型别名和接口都可以胜任。但如果需要进行继承和实现,或者在多个接口之间共享属性和方法,那么使用接口会更加合适和灵活。根据具体的需求和场景,我们可以选择适合的方式来定义和使用类型。

6.泛型的使用

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS会智能的根据传递的参数,推导出泛型的具体类型

如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值

6.1 在函数中使用泛型

在函数名之后写上<泛型名称>

function test<T>(arr: T[]): T[] {
    const newArr: T[] = [];
    // ...
    return newArr
}

const arr = test<number>([1,2,3])

6.2 在类型别名、接口、类中使用泛型

class ArrayHelper<T> {
    constructor(private arr: T[]) { }
    take(n: number): T[] {
        if (n >= this.arr.length) {
            return this.arr;
        }
        const newArr: T[] = [];
        for (let i = 0; i < n; i++) {
            newArr.push(this.arr[i]);
        }
        return newArr;
    }

    shuffle() {
        for (let i = 0; i < this.arr.length; i++) {
            const targetIndex = this.getRandom(0, this.arr.length);
            const temp = this.arr[i];
            this.arr[i] = this.arr[targetIndex];
            this.arr[targetIndex] = temp;
        }
    }

    private getRandom(min: number, max: number) {
        const dec = max - min;
        return Math.floor(Math.random() * dec + max);
    }
}

6.3 泛型约束和多泛型

// 泛型约束
interface hasNameProperty {
     name: string
}

/**
* 将某个对象的name属性的每个单词的首字母大小,然后将该对象返回
*/

function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
     obj.name = obj.name
         .split(" ")
         .map(s => s[0].toUpperCase() + s.substr(1))
         .join(" ");
     return obj;
 }

const o = {
     name:"kevin yuan",
     age:22,
     gender:"男"
}

const newO = nameToUpperCase(o);
console.log(newO.name); //Kevin Yuan

// 多泛型
//将两个数组进行混合
//[1,3,4] + ["a","b","c"] = [1, "a", 3, "b", 4, "c"]
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
    if (arr1.length != arr2.length) {
        throw new Error("两个数组长度不等");
    }
    let result: (T | K)[] = [];
    for (let i = 0; i < arr1.length; i++) {
        result.push(arr1[i]);
        result.push(arr2[i]);
    }
    return result;
}
const result = mixinArray([1, 3, 4], ["a", "b", "c"]);
result.forEach(r => console.log(r));

待续