引言
随着现代 Web 应用程序变得越来越复杂,JavaScript 作为默认的编程语言已经无法满足对代码可维护性、可扩展性和安全性的需求。在这样的背景下,TypeScript 应运而生。本文将深入探讨 TypeScript 的核心特性、优势以及如何应用 TypeScript 来提升前端开发体验。
了解 TypeScript
TypeScript 是一种由 Microsoft 开发的开源编程语言,它扩展了 JavaScript,并引入了静态类型。TypeScript 可以通过编译器转换为标准的 JavaScript 代码,同时提供了强大的工具和功能,使得开发者能够更轻松地构建和维护复杂的应用程序。
TypeScript 带来的优势
静态类型系统
在 TypeScript 中,我们可以在声明一个变量之后设置我们想要添加的类型(通常称之为“类型注释”或“类型签名”或"类型约束")。但是,如果变量有默认值的话,一般我们也不需要显式声明类型,TypeScript 会自动推断变量的类型(这就是所谓的“类型推断”)。
布尔类型 boolean
布尔类型用来表示最简单的数据类型:true 或 false 值。
const isDone: boolean = false;
// ES5:var isDone = false;
数值类型 number
数值类型用来表示数字,包括二进制数、十进制数和十六进制数。
const count: number = 10;
// ES5:var count = 10;
字符串类型 string
字符串类型用双引号或单引号表示字符串。此外,还有模板字符串,使用反引号代替普通字符串中的双引号和单引号,其中可以包含 ${xxx}
的占位符用来进行变量值的解析。
const title: string = "你好,李焕英";
// ES5:var title = '你好,李焕英';
这里,以上三种类型属于原始类型(Primitive Types)在非严格模式下,是可以赋值为 null 或者是 undefined。
const a: string = null // undefined
const b: number = null // undefined
const c: boolean = null // undefined
但是注意在严格模式下是不可以的
数组类型 array
数组类型有两种表示方法。一种是在元素类型后接上 []
表示由此类型元素组成的一个数组;另一种是使用数组泛型 Array<元素类型>
。
const list: number[] = [1, 2, 3];
// ES5:var list = [1, 2, 3];
const anotherList: Array<number> = [1, 2, 3]; // 使用 Array<number> 泛型语法
// ES5:var anotherList = [1, 2, 3];
枚举类型 enum
枚举类型可以用来定义一些带名字的常量。 TypeScript 支持数字的和基于字符串的枚举。
enum Gender {
MAN,
WOMAN
}
const gender: string = Gender[0];
console.log(gender);
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}
any 类型
有时无法确定变量的类型,可以使用 any
类型。如果一个数据是 any
类型,那么可以访问它的任意属性,即使该属性不存在。
let str: any = 666;
str = "你好,李焕英";
str = false;
元组类型 Tuple
元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。在 JavaScript 中没有元组,这是 TypeScript 中特有的类型。
let user: [string, number, string?]; // ?代表可选
user = ['李焕英', 18, '女'];
console.log(user, 'user');
user = ['贾玲她妈', 19];
console.log(user, 'user');
void 类型
void
类型表示没有任何类型。当一个函数没有返回值时,通常会见到其返回值类型是 void
。
function fn(): void {
console.log('void表示为空的,缺乏的,代表函数没有返回值');
}
const nothing: void = undefined;
null 和 undefined 类型
在 TypeScript 中,undefined
和 null
两者有各自的类型分别为 undefined
和 null
。
let u: undefined = undefined;
let n: null = null;
never 和 unknown 类型
never
类型表示那些永不存在的值的类型。它是任何其他类型的子类型,也可以赋值给任何其他类型;然而,没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。即使any
也不可以赋值给never
。
// 返回类型为 never 的函数,表示抛出异常并永远不会返回任何值
function error(message: string): never {
throw new Error(message);
}
// 返回类型为 never 的函数,表示永远不会结束的循环
function infiniteLoop(): never {
while (true) {
// 无限循环
}
}
unknown
类型是any
类型对应的安全类型。当一个变量被声明为unknown
类型时,在对该变量执行赋值操作时,所有的分配都被认为是类型正确的。换句话说,TypeScript 不允许我们直接将一个unknown
类型的值赋给其他类型,除非做了类型检查或类型断言。
let userInput: unknown;
let userName: string;
userInput = 5;
userInput = 'Hello';
// 需要进行类型检查或类型断言来确保赋值的安全性
if (typeof userInput === 'string') {
userName = userInput; // 这里的赋值是类型正确的
}
高级类型
在 TypeScript 中,高级类型为我们提供了更灵活、更强大的类型抽象和处理能力。本文将深入探讨 TypeScript 中的几种高级类型,包括联合类型、交叉类型、可选类型Partial、条件类型和映射类型。
联合类型
联合类型通常与 null
或 undefined
一起使用。在函数参数中,可以使用联合类型作为参数类型,如下所示:
const getSum = (a: number | string, b: number | string): number => {
return (+a) + (+b);
};
console.log(getSum(1, 2)); // 输出 3
console.log(getSum('1', '2')); // 输出 3
这里,a
和 b
的类型是 number | string
,意味着它们可以是 string
或 number
类型的值。
交叉类型
TypeScript 的交叉类型是将多个类型合并为一个类型。通过交叉类型,我们可以将现有的多种类型组合成一个类型,它包含了所需的所有类型的特性。例如:
interface IUser {
id: number;
name: string;
}
interface IStudent {
score: number;
}
const stu: IUser & IStudent = {
id: 1,
name: '李焕英',
score: 100
};
console.log(stu); // 输出 { id: 1, name: '李焕英', score: 100 }
在这个例子中,stu
同时具备了 IUser
和 IStudent
接口的属性。
可选类型Partial
可选类型可以将声明的属性变为可选的。我们可以使用 Partial
关键字,也可以使用 ?
将属性变为可选的。例如:
interface IPeople {
name: string;
age: number;
}
const lihuanying: Partial<IPeople> = {
name: '李焕英'
};
这样我们就实现了将 IPeople
类型映射成一个新的类型,这个新的类型中的所有属性都变为可选类型。
条件类型
条件类型简单理解就是 JavaScript 中的三元表达式,只不过它获取的是类型而不是值。例如:
type TAnimal<T> = T extends Fish ? Water : Sky;
let con1: TAnimal<Bird> = { name4: '天空' };
这里,如果 T
是 Fish
类型,那么 TAnimal<T>
将会是 Water
,否则就是 Sky
。
映射类型
TypeScript 中的映射类型和数学中的映射类似,能够将一个类型映射成另一个类型。常见的映射类型包括拷贝、可选、必填、只读等。
interface IUserInfo {
id: number;
name: string;
sex: string;
score: number;
}
const userInfo: Partial<IUserInfo> = {
name: '李焕英'
// gender: '女'
}
在这个例子中,我们将 IUserInfo
类型映射成一个新的类型,这个新的类型中的所有属性都变为可选类型。
通过这些高级类型的灵活应用,我们可以更好地定义和操作类型,使得代码更加健壮和可维护。
类型推断&类型断言&非空断言
在 TypeScript 的世界,类型推断、类型断言和非空断言是我们日常开发中经常遇到的概念。它们为我们提供了处理类型不确定性的手段,在某些情况下,它们可以极大地提高代码的可读性和可维护性。
类型推断
类型推断的含义是不需要指定变量类型或函数的返回值类型,TypeScript 可以根据一些简单的规则推断其类型。这种特性使得我们在编写代码时可以更加灵活,同时也减少了代码的冗余性。例如:
let x = 3; // TypeScript 推断 x 是 number 类型
在这个例子中,我们没有显式地声明 x
的类型,但 TypeScript 会根据赋值的情况自动推断出 x
的类型为 number
。
类型断言
类型断言主要用于当 TypeScript 推断出来类型并不满足你的需求,你需要手动指定一个类型。TypeScript 允许你覆盖它的推断,毕竟作为开发者你比编译器更了解你写的代码。使用关键字 as
进行类型断言是最常见的做法,而标签 <>
的方式容易与 JSX 语法冲突,因此建议统一使用 as
进行类型断言。示例如下:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
在这里,我们将 someValue
断言为 string
类型,以便能够调用 length
属性。
非空断言
如果编译器不能够去除 null
或 undefined
,可以使用非空断言 !
手动去除。这在处理 DOM
元素等可能为 null
或 undefined
的情况时非常有用。
const element = document.getElementById("myInput")!;
element.focus();
在这个例子中,我们使用了非空断言符号 !
来告诉 TypeScript,getElementById
肯定会返回一个非空的元素,这样我们可以直接调用 focus
方法而无需担心空值异常。
特别注意: TypeScript 无法像 JavaScript 那样访问 DOM,因此每当我们尝试访问 DOM 元素时,TypeScript 都无法确定它们是否真的存在。使用非空断言运算符 (!
) 或类型断言 (as
),我们可以明确地告诉编译器一个表达式的值不是 null
或 undefined
,或者转换类型。
总之,类型推断、类型断言和非空断言为我们在使用 TypeScript 时提供了更多的灵活性和安全性。合理地运用它们可以帮助我们更好地应对类型不确定性的情况,使得代码更加可靠和清晰。
接口(Interfaces)
接口是 TypeScript 中非常重要的概念,它类似于一种规范或契约,用于约定对象的结构。通过接口,我们可以规定对象应包含哪些成员以及这些成员的类型。
基本用法
interface Post {
title: string;
content: string;
}
function printPost(post: Post) {
console.log(post.title);
console.log(post.content);
}
printPost({
title: 'Hello TypeScript',
content: 'A JavaScript superset'
});
在上述示例中,Post
接口定义了一个对象应当具有 title
和 content
两个属性。函数 printPost
的参数 post
必须符合 Post
接口的约定。
可选成员
如果一个对象的某些成员是可有可无的,我们可以使用可选成员的特性:
interface Post {
title: string;
content: string;
subtitle?: string; // 可选成员
}
在上述示例中,subtitle
成员被标记为可选。
只读成员
接口可以包含只读成员,一旦初始化后就不能再修改:
interface Post {
title: string;
content: string;
subtitle?: string; // 可选成员
readonly summary: string; // 只读成员
}
动态成员
动态成员通常用于一些带有动态键值的对象,例如缓存对象等。
interface Cache {
[prop: string]: string; // 动态成员
}
const cache: Cache = {};
cache.foo = 'value1';
cache.bar = 'value2';
在上述例子中,我们创建了一个 Cache
接口,并实现了一个带有动态成员的 cache
对象,这些动态成员都必须符合 Cache
接口的类型约束。
总之,通过接口,我们可以明确地约定对象的结构和约束其类型。虽然在运行阶段接口本身没有意义,但它在开发阶段能够帮助我们更好地进行类型检查和约束,提高代码的可维护性和可读性。
class类
类描述了所创建的对象共同的属性和方法。通过 class
关键字声明一个类,主要包含以下模块:属性、构造函数和方法。
类的本质
在 TypeScript 中,类是一种用来创建对象的蓝图。它定义了对象共同的属性和方法。通过 class 关键字声明一个类,它包含属性、构造函数和方法。类可以被实例化为对象,然后可以访问对象的属性和方法。
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(): string {
return '我会动';
}
}
const p1 = new Animal('李焕英');
console.log(p1.move()); // 输出:"我会动"
constructor()
方法是类的默认方法,通过 new
来生成对象实例时,自动调用该方法。换句话说,constructor()
方法默认返回实例对象 this
。
类的继承
类继承是一种基于类的程序设计模式,允许使用继承来扩展现有的类,以便子类可以复用父类的公共部分。在 TypeScript 中,通过 extends 关键字实现类的继承。子类会继承父类的属性和方法,同时可以添加自己的属性和方法。
使用 extends
关键字来实现继承:
// 继承 Animal 类
class People extends Animal {
color: string;
constructor(name: string, color: string) {
super(name); // 调用父类的构造函数
this.color = color;
}
coding(): string {
return '我会写代码';
}
}
const p2 = new People('张三峰', '黄皮肤');
console.log(p2.move()); // 输出:"我会动"
console.log(p2.coding()); // 输出:"我会写代码"
类方法重载
类方法重载是指在类中对方法进行多态的一种表现形式。在 TypeScript 中,方法重载可以根据不同的参数类型执行不同的操作。这使得方法可以根据传入的参数类型进行不同的逻辑处理。比如,在以下示例中我们重载了 Animal1
类的 move
成员方法:
class Coder {
coding(): void;
coding(type: string): void;
coding(type: string[]): void;
coding(type?: string | string[]) {
if (typeof type === 'string') {
console.log('我会写' + type);
} else if (!type) {
console.log('我会敲代码');
} else {
console.log('我会写' + type.join(','));
}
}
}
const c = new Coder();
c.coding();
c.coding('javascript');
c.coding(['html', 'css', 'javascript']);
类的修饰符
作用: 对类的属性和方法提供权限控制
- public: 公有的可以在任何地方可以被访问和修改(类中属性和方法默认为 public)
- private: 私有的仅在当前类可访问和修改
- protected : 仅当前类与子类(继承类)可以访问和修改
- readonly: 只读修饰符, 仅可访问不可修改
class Animal2 {
public name: string;
readonly body: boolean;
protected color: string;
private height: number;
constructor(name: string, body: boolean, color: string, height: number) {
this.name = name;
this.body = body;
this.color = color;
this.height = height;
}
say(): void {
console.log(`我叫${this.name},我有${this.body},我皮肤是${this.color},我身高${this.height}`);
}
}
const p4 = new Animal2('李焕英', true, '黄皮肤', 180);
静态属性和静态方法
静态属性和静态方法不需要实例化类即可访问,它们属于类本身而不是类的实例。这意味着静态属性和方法不依赖于类的实例,可以直接通过类名进行访问和调用。静态属性和方法通常用于表示与类本身相关的行为或特征。
class Animal3 {
// 定义静态属性
static categoies: string[] = ['人类','中国人']
// 定义静态方法
static isChinese(a: any) {
return a instanceof Animal3 // 判断 a 是否为 Animal 的实例
}
}
// 注意! 访问 静态属性和方法不需要实例化
// 访问静态属性 categoies
console.log(Animal3.categoies)
以上都是 TypeScript 中类的重要概念,在实际开发中,它们可以帮助我们更好地组织代码并实现面向对象编程中的各种特性。
泛型约束
泛型在 TypeScript 中提供了一种定义函数、接口和类的方式,而不需要指定具体类型。相反,在使用函数、接口或类时才会定义类型。这个特性允许高度的代码重用性。
不使用泛型:
让我们以创建特定类型的数组为例。
// 定义一个创建 number 类型的方法
function createNumberArray(length: number, value: number): number[] {
const arr = Array<number>(length).fill(value);
return arr;
}
// 定义一个创建 string 类型的方法
function createStringArray(length: number, value: string): string[] {
const arr = Array<string>(length).fill(value);
return arr;
}
const numArr = createNumberArray(3, 100);
const strArr = createStringArray(3, 'foo');
使用泛型:
通过引入泛型参数 T
,我们可以在函数内部代表未指定的类型,并在调用函数时指定类型。
function createArray<T>(length: number, value: T): T[] {
const arr = Array<T>(length).fill(value);
return arr;
}
const numArr = createArray<number>(3, 100);
const strArr = createArray<string>(3, 'foo');
在这种情况下,T
表示在调用函数时将确定的类型。这种方法利用泛型的能力创建灵活且可重用的函数,可以处理各种数据类型。
在 TypeScript 中,Array
类本身就是一个泛型类型。当定义 Array
类型时,它不知道将存储什么类型的数据,因此使用了泛型参数。在使用 Array
类型时指定了该参数,展现了泛型的本质。总之,泛型使我们的代码更加抽象和可重用,可以推迟类型的具体指定直到使用时。通过使用泛型,我们可以编写更加灵活和通用的代码,适用于不同的数据类型。
工具类型
在 TypeScript 中,工具类型提供了一系列强大的功能,可以帮助我们操作和转换现有的类型。这些工具类型允许我们以更高级的方式操作类型,使得代码更加灵活、可维护和安全。
Partial< T >
Partial
是一个预定义的 TypeScript 工具类型,它接受一个类型 T
并返回一个新类型,其中 T
的所有属性变为可选。这样的特性在创建包含可选属性的对象时非常有用。
interface User {
name: string;
age: number;
}
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
const initialUser: User = { name: "Alice", age: 30 };
const updatedUser = updateUser(initialUser, { age: 31 });
Required< T >
与 Partial
相反的是 Required
工具类型,它接受类型 T
并返回一个新类型,该类型中 T
的所有属性都变为必填项。
interface Props {
name?: string;
age?: number;
}
function validateProps(props: Required<Props>) {
// ...
}
Pick<T, K>
Pick
工具类型接受两个参数:类型 T
和 K
,它从类型 T
中挑选出指定的属性集合 K
构造出一个新类型。
interface Car {
make: string;
model: string;
year: number;
}
type CarSummary = Pick<Car, "make" | "model">;
Omit<T, K>
相对于 Pick
,Omit
接受类型 T
和 K
,但返回结果是从 T
中剔除了指定属性集合 K
后的新类型。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;
ReturnType< T >
ReturnType
工具类型接受一个函数类型,并返回其返回值的类型。
function greet(): string {
return "Hello!";
}
type GreetReturnType = ReturnType<typeof greet>; // GreetReturnType is string
以上是我觉得平时比较多见的方法,更多的详细方法 ===》工具类型
通过使用 TypeScript 提供的工具类型,我们可以以一种更高级、更复杂的方式操作和利用类型系统,从而增强代码的健壮性和可读性。这些工具类型提供了便捷的方式来创建、转换和操纵类型,使得 TypeScript 在静态类型检查方面能够提供更多的支持,确保编写的代码更加可靠。
结语
TypeScript 是一个强大的工具,它能够改善前端开发体验,提高代码质量并降低维护成本。引入了静态类型、类型推断和类似功能,TypeScript 为 JavaScript 带来了更强大的表现力。在当今快速发展的 Web 开发领域,TypeScript 无疑是一种值得学习和应用的技术。通过本文的介绍,希望读者能够更加深入地了解 TypeScript,并在实际项目中应用它,从而提升自身的开发能力和项目质量。