从TypeScript诞生之初,我就有在关注学习,当时还写了两篇相关介绍文章,尽管那个时候的我并不确定这个所谓的JavaScrip超集,是否会跟其他前端新技术一样,大家追捧一阵,随后便迅速消失在无人关注的角落里,但这么多年过去了,我想它的重要性已经成为任何一个前端的必备技术了。
想要真正掌握一门语言或框架,运用在实际项目中是最快的方法,而自己写的 Demo或者个人项目,是不具备真正的用户群体和广泛的业务场景,很多问题和局限性是难以发现的。但跟很多新技术一样,往往只能在新项目或是原项目的部分场景进行尝试,这里面又涉及到项目定位、开发周期和招聘成本等等一系列问题,我相信这一点大家都有或多或少的共鸣。
有些同学在大厂面试中滔滔不绝地分享自己对新技术的掌握程度和实践经验,满怀信心地期待加入公司后能大展身手,然而,真正进入项目组后,却发现大部分时间都花在维护使用 jQuery 或PHP开发的旧项目上。这种强烈的落差感和无力感,就像是《三体》里的那句——现实的引力实在是太沉重了。
去年,因原项目性能受限于原框架的局限性,决定重新开发新项目,而我在跟团队成员讨论之后,决定引入TypeScript,尽管新项目仍在开发还未上线,但TypeScript已经帮助我们发现不少原项目里的问题。TypeScript 的内容有很多,或许一下子让人无从下手,但在实际项目中用到的特性其实并没有这么多,所以想在这篇文章中跟大家分享我们项目目前使用到的一些特性,以及踩过的一些坑。
npm i -g typescript
tsc -v
tsc fileName.ts
TypeScript 的强类型检查是其一大优势,通过明确的类型注解,能够帮助开发者在代码中区分不同的数据类型,避免潜在的错误。这次项目重构过程中,我发现好几处 Number 和 String 类型混淆的地方,若是处理较长的数字(如 ID)时,如果误将其视为 Number,可能会因精度丢失引发 Bug。
let name: string = "NianGao";
let age: number = 17;
TypeScript 支持对数组和元组进行类型定义,确保数据的一致性和可预测性。声明数组类型的方式:类型+方括号,下面的number[]
表示一个包含数字类型元素的数组;元祖是一种特殊的数组类型,它允许你指定一个固定长度和特定类型顺序的数组,特别注意:元组的长度是固定的,不能在运行时动态添加或删除元素
let numbers: number[] = [1, 2, 3];
// 定义用户信息的元组
let arr: [string, number] = ['NianGao', 17];
在我们的项目中,Interface 是使用最多,同时也是最枯燥的部分。在上一篇文章TypeScript + 微信小程序:构建高效可维护的项目中,我分享了一个 API 请求封装,于是我们写了大量的接口数据定义对应的 Interface,甚至在对数据进行二次处理时,还可能需要定义新的 Interface。虽然很多同学可能不太愿意花时间去写这些内容,但一旦完成并规范化,后续的维护和开发中你会真正体会到它带来的无限便利。
interface IUser {
readonly id: number; // 只读属性,不能修改
name: string;
age?: number; // 可选属性
}
let user: IUser = { id: 13, name: "NianGao", age: 17 };
user.name = "年糕"; // 可以修改
// user.id = 2; // 错误,id 是只读属性
// 约定输入和返回值为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
}
使用函数类型来声明变量或对象的属性,以描述函数的形状。
// 函数类型别名 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
关键字来创建类型别名。
type MathOperation = (x: number, y: number) => number;
这里的 MathOperation 是一个类型别名,它表示一种函数类型。具体来说,MathOperation 是一个接受两个参数(均为数字类型)并返回一个数字的函数类型。使用类型别名的主要好处之一是可以重复使用这个别名,使代码更简洁。例如,你可以在多个地方使用 MathOperation 来声明接受相同参数和返回相同类型的函数
let add: MathOperation = (x, y) => x + y;
let subtract: MathOperation = (x, y) => x - y;
写到这里,不知道大家是否和我当初一样,有过这样的疑惑:面对这么多的冒号,该如何区分它们的作用?更别提其中还夹杂着等号和问号,让人一头雾水。在这里有一个简单的记忆诀窍——在 TypeScript 中,冒号后面都是在声明类型。
箭头在类型声明中用于指定函数类型,而箭头函数表达式可以用于实现具有特定类型的函数。
// 声明函数类型
// 箭头表示一个函数类型,该类型接受两个参数(x 和 y,均为数字类型),并返回一个数字
type MathOperation = (x: number, y: number) => number;
// 箭头函数表达式
// 箭头用于定义一个箭头函数,该函数具有与上述声明的 MathOperation 类型相匹配的签名。这是一种简写形式,其中 TypeScript 根据上下文推断出函数的类型
let add: MathOperation = (x, y) => x + y;
联合类型允许一个变量具有多种可能的类型。
let myVar: string | number;
myVar = "NianGao";
myVar = 17;
类型断言是在某些情况下,开发者需要告诉 TypeScript 编译器某个值的具体类型。
let name: any = "NianGao";
let strLength: number = (name as string).length;
// 或者使用尖括号语法
let strLengthA: number = (<string>name).length;
下面这个例子就是 TypeScript 会将 setTimeout 识别为Timeout
对象,所以需要临时转换为未知类型 unknown,再类型断言为 number
let timer: number | null = setTimeout(() => {
clearTimeout(timer!);
timer = null;
this.setData({
showNextCoupon: true,
});
}, 300) as unknown as number;
类是一种模板,用于创建对象,并定义对象的行为和状态,类可以包含构造函数、属性、方法等成员。
public
、private
和 protected
等访问修饰符,用于控制类成员的可见性和访问权限public
修饰的属性或方法是公有的, 可以在任何地方被访问到, 默认所有的属性和方法都是public
的
private
修饰的属性或方法是私有的, 不能在声明它的类的外部访问
protected
修饰的属性或方法是受保护的, 它和private
类似, 区别是它在子类中也是允许被访问的
a. 封装 Encapsulation
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
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
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()); // 多态性,根据对象的类型调用不同的方法
}
1. 在构造函数中使用 super
在子类的构造函数中使用super
用于调用父类的构造函数。这是必须的,因为子类可能需要执行一些额外的初始化工作,而父类的构造函数通常包含了一些基础的初始化逻辑。
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
可以用于调用父类的同名方法。这样子类可以在重写父类方法时执行一些额外的逻辑。
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
可以用于调用父类的静态方法。
class Animal {
static getType(): string {
return "Generic Animal";
}
}
class Dog extends Animal {
static getType(): string {
return super.getType() + " - Dog";
}
}
console.log(Dog.getType()); // 调用的是 Dog 类的静态方法,并在其中调用了父类的静态方法
数字枚举
默认情况下,数字枚举的值从 0 开始自增,也可以手动指定枚举值。
enum Direction {
Up = 1,
Down,
Left,
Right,
}
字符串枚举
字符串枚举的每个成员都必须用字符串字面量或另一个字符串枚举成员初始化。
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
在枚举中,值和名称之间存在双向映射,可以通过值获取名称,也可以通过名称获取值。
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
关键字,可以将枚举声明为常量枚举,常量枚举在编译阶段会被删除,只留下使用到的值,这可以减小代码体积,但失去了动态特性。
const enum Status {
Active,
Inactive,
}
let myStatus: Status = Status.Active; // 编译后直接替换成数字 0
泛型让我们在定义函数、接口或类的时候, 不预先指定具体的类型, 而在使用的时候再指定类型,这使得代码更具可复用性和灵活性。需要注意的是,泛型中的T(Type)只是一个常见的命名习惯,你也可以使用其他命名方式。
泛型函数
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)
泛型类
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)
泛型接口
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 属性
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. 声明数组类型的方式 - 类型+方括号
let arr: number[] = [1, 2, 3];
b. 使用了 Array 泛型类型的语法,Array 是一个泛型类
let arr2: Array<number> = [1, 2, 3];
MyString 是一个字符串类型的别名。
type MyString = string;
Container 是一个泛型类型别名,它接受一个类型参数 T,通过使用 NumberContainer 别名,我们创建了一个特定类型为 number 的容器。
type Container<T> = { value: T };
type NumberContainer = Container<number>;
let numContainer: NumberContainer = { value: 13 };
Status 是一个字符串字面量联合类型的别名。
type Status = "success" | "error";
let status: Status = "success";
GreetFunction 是一个函数类型的别名,表示接受一个字符串参数并返回 void 的函数。
type GreetFunction = (name: string) => void;
const greet: GreetFunction = (name) => {
console.log(`Hello, ${name}!`);
};
greet("NianGao");
交叉类型允许我们组合多个类型,创建一个同时满足多个类型条件的类型。
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 }
行文过程中出现错误或不妥之处在所难免,希望大家能够给予指正,以免误导更多人,最后,如果你觉得我的文章写的还不错,希望能够点一下👍🏻和⭐️
OTZ!拜托了!这对我真的很重要!!!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。