希望通过本文的介绍和分析,能够让大家对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 使用模块化
- 使用模块化的方式组织代码,避免全局命名空间的污染。
- 使用
export和import关键字明确导出和引入模块。
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)时,可以考虑以下几个方面:
-
对象的形状 vs. 可重用的类型
- 如果你需要描述一个对象的形状,包括属性、方法和索引签名等,那么使用接口是一个很好的选择。
- 如果你需要创建可重用的类型,或者需要定义复杂的联合类型、交叉类型等,那么类型别名可以更好地满足这些需求。
-
继承 vs. 联合类型
- 如果你需要在多个接口之间共享属性和方法,以及进行接口的继承和实现,那么使用接口是更合适的。
- 如果你需要将多个类型组合成一个类型,可以使用联合类型或交叉类型,这时类型别名更适合。
-
扩展性 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));