前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >TypeScript 在实际项目中的应用#2024年度实用技巧

TypeScript 在实际项目中的应用#2024年度实用技巧

原创
作者头像
Nian糕
修改2025-02-05 20:54:57
修改2025-02-05 20:54:57
740
举报
文章被收录于专栏:JavaScript高阶应用
Unsplash
Unsplash

从TypeScript诞生之初,我就有在关注学习,当时还写了两篇相关介绍文章,尽管那个时候的我并不确定这个所谓的JavaScrip超集,是否会跟其他前端新技术一样,大家追捧一阵,随后便迅速消失在无人关注的角落里,但这么多年过去了,我想它的重要性已经成为任何一个前端的必备技术了。

想要真正掌握一门语言或框架,运用在实际项目中是最快的方法,而自己写的 Demo或者个人项目,是不具备真正的用户群体和广泛的业务场景,很多问题和局限性是难以发现的。但跟很多新技术一样,往往只能在新项目或是原项目的部分场景进行尝试,这里面又涉及到项目定位、开发周期和招聘成本等等一系列问题,我相信这一点大家都有或多或少的共鸣。

有些同学在大厂面试中滔滔不绝地分享自己对新技术的掌握程度和实践经验,满怀信心地期待加入公司后能大展身手,然而,真正进入项目组后,却发现大部分时间都花在维护使用 jQuery 或PHP开发的旧项目上。这种强烈的落差感和无力感,就像是《三体》里的那句——现实的引力实在是太沉重了。

去年,因原项目性能受限于原框架的局限性,决定重新开发新项目,而我在跟团队成员讨论之后,决定引入TypeScript,尽管新项目仍在开发还未上线,但TypeScript已经帮助我们发现不少原项目里的问题。TypeScript 的内容有很多,或许一下子让人无从下手,但在实际项目中用到的特性其实并没有这么多,所以想在这篇文章中跟大家分享我们项目目前使用到的一些特性,以及踩过的一些坑。

TypeScript的安装、查看、编译

代码语言:txt
复制
npm i -g typescript
tsc -v
tsc fileName.ts

数据类型与类型注解

TypeScript 的强类型检查是其一大优势,通过明确的类型注解,能够帮助开发者在代码中区分不同的数据类型,避免潜在的错误。这次项目重构过程中,我发现好几处 Number 和 String 类型混淆的地方,若是处理较长的数字(如 ID)时,如果误将其视为 Number,可能会因精度丢失引发 Bug。

代码语言:typescript
复制
let name: string = "NianGao";
let age: number = 17;
  • 数组Array & 元组Tuple

TypeScript 支持对数组和元组进行类型定义,确保数据的一致性和可预测性。声明数组类型的方式:类型+方括号,下面的number[]表示一个包含数字类型元素的数组;元祖是一种特殊的数组类型,它允许你指定一个固定长度和特定类型顺序的数组,特别注意:元组的长度是固定的,不能在运行时动态添加或删除元素

代码语言:typescript
复制
let numbers: number[] = [1, 2, 3];
// 定义用户信息的元组
let arr: [string, number] = ['NianGao', 17];

接口 Interface 用于定义对象的结构、形状和行为的抽象类型

在我们的项目中,Interface 是使用最多,同时也是最枯燥的部分。在上一篇文章TypeScript + 微信小程序:构建高效可维护的项目中,我分享了一个 API 请求封装,于是我们写了大量的接口数据定义对应的 Interface,甚至在对数据进行二次处理时,还可能需要定义新的 Interface。虽然很多同学可能不太愿意花时间去写这些内容,但一旦完成并规范化,后续的维护和开发中你会真正体会到它带来的无限便利。

代码语言:typescript
复制
interface IUser {
  readonly id: number;  // 只读属性,不能修改
  name: string;
  age?: number;  // 可选属性
}

let user: IUser = { id: 13, name: "NianGao", age: 17 };
user.name = "年糕";  // 可以修改
// user.id = 2;  // 错误,id 是只读属性

函数声明

代码语言:typescript
复制
// 约定输入和返回值为number,y为可选参数,z为默认参数
function add(x:number, y?:number, z:number = 2):number {
  if (typeof y === 'number') return x+y+z
  else return x+z
}

const add = (x:number, y?:number, z:number = 2):number => {
  if (typeof y === 'number') return x+y+z
  else return x+z
}

函数类型

使用函数类型来声明变量或对象的属性,以描述函数的形状。

代码语言:typescript
复制
// 函数类型别名 add2,表示一个接受与 add 函数相同类型的参数并返回数字的函数
let add2:(x:number, y?:number, z:number = 2) => number = add

// interface 描述函数类型
interface ISum {
  (x:number, y?:number, z:number = 2): number
}
let add2: ISum = add

这里我们还可以通过type关键字来创建类型别名。

代码语言:typescript
复制
type MathOperation = (x: number, y: number) => number;

这里的 MathOperation 是一个类型别名,它表示一种函数类型。具体来说,MathOperation 是一个接受两个参数(均为数字类型)并返回一个数字的函数类型。使用类型别名的主要好处之一是可以重复使用这个别名,使代码更简洁。例如,你可以在多个地方使用 MathOperation 来声明接受相同参数和返回相同类型的函数

代码语言:typescript
复制
let add: MathOperation = (x, y) => x + y;
let subtract: MathOperation = (x, y) => x - y;

写到这里,不知道大家是否和我当初一样,有过这样的疑惑:面对这么多的冒号,该如何区分它们的作用?更别提其中还夹杂着等号和问号,让人一头雾水。在这里有一个简单的记忆诀窍——在 TypeScript 中,冒号后面都是在声明类型

箭头在 TypeScript 中用于声明函数类型和创建箭头函数

箭头在类型声明中用于指定函数类型,而箭头函数表达式可以用于实现具有特定类型的函数。

代码语言:typescript
复制
// 声明函数类型
// 箭头表示一个函数类型,该类型接受两个参数(x 和 y,均为数字类型),并返回一个数字
type MathOperation = (x: number, y: number) => number;

// 箭头函数表达式
// 箭头用于定义一个箭头函数,该函数具有与上述声明的 MathOperation 类型相匹配的签名。这是一种简写形式,其中 TypeScript 根据上下文推断出函数的类型
let add: MathOperation = (x, y) => x + y;

联合类型&类型断言

联合类型允许一个变量具有多种可能的类型。

代码语言:typescript
复制
let myVar: string | number;
myVar = "NianGao";
myVar = 17;

类型断言是在某些情况下,开发者需要告诉 TypeScript 编译器某个值的具体类型。

代码语言:typescript
复制
let name: any = "NianGao";
let strLength: number = (name as string).length;
// 或者使用尖括号语法
let strLengthA: number = (<string>name).length;

下面这个例子就是 TypeScript 会将 setTimeout 识别为Timeout对象,所以需要临时转换为未知类型 unknown,再类型断言为 number

代码语言:typescript
复制
let timer: number | null = setTimeout(() => {
    clearTimeout(timer!);
    timer = null;
    this.setData({
        showNextCoupon: true,
    });
}, 300) as unknown as number;

类 Class

类是一种模板,用于创建对象,并定义对象的行为和状态,类可以包含构造函数、属性、方法等成员。

  • a. 构造函数 Constructor: 构造函数在对象实例化时被调用,用于初始化对象的属性
  • b. 属性 Properties: 类中可以定义各种属性,这些属性用于存储对象的状态
  • c. 方法 Methods: 方法是类中的函数,用于定义对象的行为
  • d. 访问修饰符 Access Modifiers: TypeScript 提供了publicprivateprotected等访问修饰符,用于控制类成员的可见性和访问权限

public修饰的属性或方法是公有的, 可以在任何地方被访问到, 默认所有的属性和方法都是public

private修饰的属性或方法是私有的, 不能在声明它的类的外部访问

protected修饰的属性或方法是受保护的, 它和private类似, 区别是它在子类中也是允许被访问的

面向对象编程三个特点——封装、继承、多态

a. 封装 Encapsulation

  • 封装是将对象的状态(属性)和行为(方法)捆绑在一起,形成一个独立的单元
  • 对外部隐藏对象的内部实现细节,只暴露必要的接口
  • 通过封装,可以控制对对象的访问,提高代码的安全性和可维护性
代码语言:typescript
复制
class BankAccount {
    private balance: number;
    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }
    deposit(amount: number): void {
        this.balance += amount;
    }
    withdraw(amount: number): void {
        if (amount <= this.balance) {
            this.balance -= amount;
        } else {
            console.log("Insufficient funds");
        }
    }
    getBalance(): number {
        return this.balance;
    }
}

let account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 封装了对balance的访问

b. 继承 Inheritance

  • 继承是一种通过创建新的类(子类)来重用现有类(父类)的属性和方法的机制
  • 子类继承父类的特性,可以添加新的属性和方法,也可以重写父类的方法
  • 提高代码的可重用性和扩展性
代码语言:typescript
复制
class Animal {
    makeSound(): void {
        console.log("Some generic sound");
    }
}

class Dog extends Animal {
    makeSound(): void {
        console.log("Woof! Woof!");
    }
    fetch(): void {
        console.log("Fetching the ball");
    }
}

let myDog = new Dog();
myDog.makeSound(); // 多态性,调用的是Dog类的方法

c. 多态 Polymorphism

  • 多态性允许对象以多种形式(类型)存在,即同一个方法可以根据对象的具体类型表现出不同的行为
  • 提高灵活性,允许使用基类类型的变量引用派生类对象,从而实现对不同对象的统一处理
代码语言:typescript
复制
class Shape {
    calculateArea(): number {
        return 0;
    }
}

class Circle extends Shape {
    constructor(private radius: number) {
        super();
    }
    calculateArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}

class Rectangle extends Shape {
    constructor(private width: number, private height: number) {
        super();
    }
    calculateArea(): number {
        return this.width * this.height;
    }
}

let shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];
for (let shape of shapes) {
    console.log(shape.calculateArea()); // 多态性,根据对象的类型调用不同的方法
}

super 是一个关键字,用于在子类中调用父类的方法或访问父类的属性

1. 在构造函数中使用 super

在子类的构造函数中使用super用于调用父类的构造函数。这是必须的,因为子类可能需要执行一些额外的初始化工作,而父类的构造函数通常包含了一些基础的初始化逻辑。

代码语言:typescript
复制
class Animal {
    constructor(public name: string) {
        console.log(`${name} is created.`);
    }
}

class Dog extends Animal {
    private breed: string;
    constructor(name: string, breed: string) {
        super(name); // 调用父类的构造函数
        this.breed = breed;
    }
}

let myDog = new Dog("Buddy", "Golden Retriever");
console.log(myDog.name); // 通过继承得到的属性

2. 在普通方法中使用 super

在子类的普通方法中,super可以用于调用父类的同名方法。这样子类可以在重写父类方法时执行一些额外的逻辑。

代码语言:typescript
复制
class Animal {
    makeSound(): void {
        console.log("Some generic sound");
    }
}

class Dog extends Animal {
    makeSound(): void {
        super.makeSound(); // 调用父类的方法
        console.log("Woof! Woof!");
    }
}

let myDog = new Dog();
myDog.makeSound(); // 调用的是 Dog 类的方法,并在其中调用了父类的方法

3. 在静态方法中使用 super

在子类的静态方法中,super可以用于调用父类的静态方法。

代码语言:typescript
复制
class Animal {
    static getType(): string {
        return "Generic Animal";
    }
}

class Dog extends Animal {
    static getType(): string {
        return super.getType() + " - Dog";
    }
}

console.log(Dog.getType()); // 调用的是 Dog 类的静态方法,并在其中调用了父类的静态方法

枚举 Enum 是一种用于命名一组命名常量的数据类型

数字枚举

默认情况下,数字枚举的值从 0 开始自增,也可以手动指定枚举值。

代码语言:typescript
复制
enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}

字符串枚举

字符串枚举的每个成员都必须用字符串字面量或另一个字符串枚举成员初始化。

代码语言:typescript
复制
enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',
}

在枚举中,值和名称之间存在双向映射,可以通过值获取名称,也可以通过名称获取值。

代码语言:typescript
复制
enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}

let directionName: string = Direction[2]; // 获取值为2的枚举项的名称
console.log(directionName); // "Down"

let directionValue: number = Direction.Left; // 获取枚举项的值
console.log(directionValue); // 3

常量枚举

通过使用const关键字,可以将枚举声明为常量枚举,常量枚举在编译阶段会被删除,只留下使用到的值,这可以减小代码体积,但失去了动态特性。

代码语言:typescript
复制
const enum Status {
    Active,
    Inactive,
}

let myStatus: Status = Status.Active; // 编译后直接替换成数字 0

泛型 Generics 用于创建可重用、灵活且类型安全的组件的机制

泛型让我们在定义函数、接口或类的时候, 不预先指定具体的类型, 而在使用的时候再指定类型,这使得代码更具可复用性和灵活性。需要注意的是,泛型中的T(Type)只是一个常见的命名习惯,你也可以使用其他命名方式。

泛型函数

代码语言:typescript
复制
function identity<T>(arg: T): T {
    return arg;
}

// 使用泛型函数,通过使用 <string>,我们告诉编译器 T 应该是字符串类型
let result = identity<string>("Hello, TypeScript!");
console.log(result); // "Hello, TypeScript!"

// 类型推论 - 没有明确的指定类型的时候推测出一个类型
let result1 = identity(123)
let result2 = identity(true)

泛型类

代码语言:typescript
复制
class Queue<T> {
    private data = [];
    push(item: T) {
        return this.data.push(item)
    }
    pop():T {
        return this.data.shift()
    }
}

// 在实例化时,我们通过 <number> 指定了 T 为数字类型
const queue = new Queue<number>()
queue.push(1)

泛型接口

代码语言:typescript
复制
interface Pair<T, U> {
    first: T;
    second: U;
}

let pair: Pair<number, string> = { first: 1, second: "two" };
console.log(pair); // { first: 1, second: "two" }

有时需要对泛型进行更精确的控制,这时可以使用泛型约束,指定泛型参数必须满足的条件。下面这个例子,Lengthwise 是一个包含 length 属性的接口,logLength 函数使用泛型约束<T extends Lengthwise>,确保传递给它的参数必须包含 length 属性

代码语言:typescript
复制
interface Lengthwise {
    length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
    console.log(arg.length);
}

logLength("Hello"); // 5
logLength([1, 2, 3]); // 3
// logLength(42); // 编译错误,因为数字没有 length 属性

这里在回过头扩展一下,有了泛型,我们声明一个包含数字的数组,就有下面两种方式:

a. 声明数组类型的方式 - 类型+方括号

代码语言:typescript
复制
let arr: number[] = [1, 2, 3];

b. 使用了 Array 泛型类型的语法,Array 是一个泛型类

代码语言:typescript
复制
let arr2: Array<number> = [1, 2, 3];

类型别名 Type

MyString 是一个字符串类型的别名。

代码语言:typescript
复制
type MyString = string;

Container 是一个泛型类型别名,它接受一个类型参数 T,通过使用 NumberContainer 别名,我们创建了一个特定类型为 number 的容器。

代码语言:typescript
复制
type Container<T> = { value: T };
type NumberContainer = Container<number>;

let numContainer: NumberContainer = { value: 13 };

Status 是一个字符串字面量联合类型的别名。

代码语言:typescript
复制
type Status = "success" | "error";
let status: Status = "success";

GreetFunction 是一个函数类型的别名,表示接受一个字符串参数并返回 void 的函数。

代码语言:typescript
复制
type GreetFunction = (name: string) => void;
const greet: GreetFunction = (name) => {
    console.log(`Hello, ${name}!`);
};
greet("NianGao");

交叉类型

交叉类型允许我们组合多个类型,创建一个同时满足多个类型条件的类型。

代码语言:typescript
复制
type Dog = { name: string; breed: string };
type Bird = { name: string; wingspan: number };
type DogAndBird = Dog & Bird;

let pet: DogAndBird = { name: "Charlie", breed: "Labrador", wingspan: 30 };

interface IName {
    name: string
}
type IPerson = IName & { age: number }
let person:IPerson = { name: 'NianGao', age: 17 }
End of File

行文过程中出现错误或不妥之处在所难免,希望大家能够给予指正,以免误导更多人,最后,如果你觉得我的文章写的还不错,希望能够点一下👍🏻和⭐️

OTZ!拜托了!这对我真的很重要!!!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • TypeScript的安装、查看、编译
  • 数据类型与类型注解
  • 接口 Interface 用于定义对象的结构、形状和行为的抽象类型
  • 函数声明
  • 函数类型
  • 箭头在 TypeScript 中用于声明函数类型和创建箭头函数
  • 联合类型&类型断言
  • 类 Class
  • 面向对象编程三个特点——封装、继承、多态
  • super 是一个关键字,用于在子类中调用父类的方法或访问父类的属性
  • 枚举 Enum 是一种用于命名一组命名常量的数据类型
  • 泛型 Generics 用于创建可重用、灵活且类型安全的组件的机制
  • 类型别名 Type
  • 交叉类型
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档