函数
TypeScript提供了丰富的函数类型定义方式,可以对函数参数、返回值进行类型注解,从而提供了更为强大的类型检查。
函数声明
在TypeScript中,你可以在函数声明中对函数的参数和返回值进行类型注解。以下是一个例子:
function add(x: number, y: number): number {
return x + y;
}
在这个例子中,我们定义了一个add
函数,它接受两个参数x
和y
,这两个参数都是number
类型,函数的返回值也是number
类型。
如果你尝试调用这个函数并传入一个非数字类型的参数,TypeScript编译器会报错:
add("Hello", 1); // Error: Argument of type '"Hello"' is not assignable to parameter of type 'number'.
函数表达式
在JavaScript中,函数不仅可以通过函数声明的方式定义,还可以通过函数表达式定义。在TypeScript中,函数表达式也可以使用类型注解:
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
return x + y;
};
在上面的例子中,我们首先定义了myAdd
变量的类型为一个函数类型(x: number, y: number) => number
,然后将一个匿名函数赋值给myAdd
。这个匿名函数的参数x
和y
的类型是number
,返回值的类型也是number
。
可选参数和默认参数
TypeScript支持可选参数和默认参数。你可以使用?
来标记可选参数,或者使用=
来指定参数的默认值:
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams"); // ah, just right
在上面的例子中,lastName
是一个可选参数。你可以不传这个参数调用buildName
函数。
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // returns "Bob Smith"
let result2 = buildName("Bob", "Adams"); // returns "Bob Adams"
在上面的例子中,lastName
有一个默认值"Smith"。如果你不传这个参数调用buildName
函数,lastName
的值将是"Smith"。
剩余参数(Rest Parameters)
当你不知道要操作的函数会有多少个参数时,TypeScript提供了剩余参数的概念。与JavaScript一样,你可以使用三个
点...
来定义剩余参数:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
在上面的例子中,restOfName
就是剩余参数,它可以接受任意数量的参数。
this和箭头函数
箭头函数可以保留函数创建时的 this
值,而不是调用时的值。在TypeScript中,你可以使用箭头函数来确保this
的值:
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
在上面的例子中,createCardPicker
函数返回一个箭头函数,这个箭头函数可以访问创建时的this
值。
重载
在JavaScript中,根据传入不同的参数调用同一个函数,返回不同类型的值是常见的情况。TypeScript通过为同一个函数提供多个函数类型定义来实现这个功能:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
reverse(12345); // returns 54321
reverse('hello'); // returns 'olleh'
在上面的例子中,我们定义了两个重载:一个是接受number
类型的参数,另一个是接受string
类型的参数。然后我们在实现函 数中处理了这两种情况。
命名空间和模块
命名空间(Namespace)
在 TypeScript 中,命名空间是一种将代码封装在一个特定名称下的方式,以防止全局作用域污染并避免命名冲突。命名空间在 TypeScript 中非常重要,因为它们为模块化和封装提供了灵活的选项。
创建命名空间的语法如下:
namespace MyNamespace {
export const myVar: number = 10;
export function myFunction(): void {
console.log("Hello from MyNamespace");
}
}
在此例中,我们创建了一个名为MyNamespace
的命名空间,该命名空间内有一个变量myVar
和一个函数myFunction
。export
关键字允许我们从命名空间外部访问这些元素。
命名空间中的元素可以通过以下方式访问:
console.log(MyNamespace.myVar); // 输出:10
MyNamespace.myFunction(); // 输出:Hello from MyNamespace
我们也可以使用嵌套的命名空间:
namespace ParentNamespace {
export namespace ChildNamespace {
export const myVar: number = 20;
}
}
console.log(ParentNamespace.ChildNamespace.myVar); // 输出:20
命名空间(Namespace)使用场景
在 TypeScript 的早期版本中,命名空间被广泛地使用来组织和包装一组相关的代码。然而,随着 ES6 模块系统(ES6 Modules)的出现和广泛使用,命名空间的用法变得越来越少,现在被视为一种遗留的方式来组织代码。
第三方库
一些第三方库仍然使用命名空间来组织自己的代码,并提供命名空间作为库的入口点。在这种情况下,我们需要使用命名空间来访问和使用库中的类型和函数。
namespace MyLibrary {
export function myFunction() {
// ...
}
}
MyLibrary.myFunction();
兼容性
在一些遗留的 JavaScript 代码或库中,命名空间仍然是一种常见的组织代码的方式。如果我们需要与这些代码进行交互,我们可能需要创建命名空间来适应它们。
// legacy.js
var MyNamespace = {
myFunction: function() {
// ...
}
};
MyNamespace.myFunction();
在上面的示例中,我们演示了命名空间的几个使用场景。第一个示例展示了如何使用命名空间访问和使用第三方库的函数。第二个示例展示了如何使用命名空间来管理全局状态。第三个示例展示了如何在与遗留 JavaScript 代码进行交互时创建命名空间。
虽然在现代 TypeScript 开发中,模块是更常见和推荐的代码组织方式,但命名空间仍然在特定的情况下具有一定的用处,并且在与一些特定的库或代码进行交互时可能是必需的。
模块
在 TypeScript 中,模块是另一种组织代码的方式,但它们更关注的是依赖管理。每个模块都有其自己的作用域,并且只有明确地导出的部分才能在其他模块中访问。
创建和使用模块的方式如下:
在myModule.ts
文件中:
export const myVar: number = 10;
export function myFunction(): void {
console.log("Hello from myModule");
}
在另一个文件中导入和使用模块:
import { myVar, myFunction } from './myModule';
console.log(myVar); // 输出:10
myFunction(); // 输出:Hello from myModule
在 TypeScript 中,我们可以使用模块解析策略(如 Node 或 Classic),以确定如何查找模块。这些策略在 tsconfig.json
文件的 compilerOptions
选项下的 moduleResolution
选项中定义。
3. 命名空间与模块的对比
虽然命名空间和模块在某种程度上有所相似,但它们有以下几个关键区别:
-
作用域:命名空间是在全局作用域中定义的,而模块则在自己的作用域中定义。这意味着,在模块内部定义的所有内容默认情况下在模块外部是不可见的,除非显式地导出它们。
-
文件组织:命名空间通常用于组织在同一文件中的代码,而模块则是跨文件进行组织。
-
依赖管理:模块关注的是如何导入和导出功能,以便管理代码之间的依赖关系。相比之下,命名空间并未对依赖管理提供明确的支持。
-
使用场景:随着 ES6 模块语法的普及,现代 JavaScript 项目通常更倾向于使用模块来组织代码。然而,对于一些遗留项目或那些需要将多个文件合并为一个全局可用的库的场景,命名空间可能更为合适。
扩展类型定义
在 TypeScript 中,我们可以通过声明文件(.d.ts
文件)来为现有的 JavaScript 库提供类型定义,或者为现有的类型添加额外的属性和方法。这个过程通常被称为“类型声明扩展”。在这篇文章中,我们将详细探讨如何通过声明文件扩展类型定义。
什么是声明文件?
在 TypeScript 中,声明文件是一种以 .d.ts
为扩展名的特殊文件,它不包含具体的实现,只包含类型声明。这些文件通常用来为已有的 JavaScript 库提供类型定义,使得我们可以在 TypeScript 代码中更安全、更方便地使用这些库。
声明文件的主要内容是类型声明,包括变量、函数、类、接口等的类型定义。这些类型声明提供了一种描述 JavaScript 代码的结构和行为的方式,使得 TypeScript 编译器能够理解和检查 JavaScript 代码。
例如,以下是一个简单的声明文件的例子:
// types.d.ts
declare var foo: string;
declare function bar(baz: number): boolean;
在上面的声明文件中,我们声明了一个全局变量 foo
和一个全局函数 bar
,并分别给它们提供了类型声明。
declare
当我们在 TypeScript 中编写声明文件时,我们使用 declare
关键字来声明全局变量、函数、类、接口等类型。
declare
关键字用于告诉 TypeScript 编译器某个标识符的类型,而不需要实际的实现代码。它用于在声明文件中描述 JavaScript 代码的类型。
下面是一些常见的用法:
1. 声明全局变量:
declare const myGlobal: string;
这个声明告诉 TypeScript 编译器,存在一个名为 myGlobal
的全局变量,它的类型是 string
。
2. 声明全局函数:
declare function myFunction(arg: number): string;
这个声明告诉 TypeScript 编译器,存在一个名为 myFunction
的全局函数,它接受一个 number
类型的参数,并返回一个 string
类型的值。
3. 声明全局类:
declare class MyClass {
constructor(name: string);
getName(): string;
}
这个声明告诉 TypeScript 编译器,存在一个名为 MyClass
的全局类,它有一个接受 string
类型参数的构造函数,并且有一个返回 string
类型的 getName
方法。
4. 声明命名空间
declare namespace MyNamespace {
export const myVariable: number;
export function myFunction(): void;
}
这个声明告诉 TypeScript 编译器,存在一个名为 MyNamespace
的全局模块/命名空间,它包含一个名为 myVariable
的变量和一个名为 myFunction
的函数。
通过使用 declare
关键字,我们可以在声明文件中描述出我们所需要的类型信息,以便 TypeScript 编译器进行类型检查和类型推断。
需要注意的是,declare
关键字只用于类型声明,不包含具体的实现代码。在使用声明文件时,我们需要确保提供了实际的实现代码,以便程序在运行时可以访问到所声明的类型。
5. 声明模块
当我们在声明文件中使用 declare module
时,我们可以定义一个模块,并在其中声明模块内部的类型。这样,其他文件在导入该模块时,就可以按照模块的名称来引用其中的类型。
declare module 'my-module' {
export const myVariable: string;
export function myFunction(): void;
}
在这个示例中,我们在 my-module
模块中声明了一个名为 myVariable
的变量和一个名为 myFunction
的函数,并通过 export
关键字将它们导出,使其在导入该模块时可见。
通过声明文件扩展类型定义
在某些情况下,我们可能需要为已有的类型添加额外的属性或方法。比如,我们可能在使用一个库时发现它缺少一些我们需要的类型定义,或者我们可能想要为一些内置类型(如 string
或 Array
)添加一些自定义的方法。
这时,我们可以通过在声明文件中使用“声明合并”(Declaration Merging)来扩展类型定义。声明合并是 TypeScript 的一项特性,它允许我们在多个位置声明同名的类型,然后 TypeScript 会将这些声明合并为一个类型。
例如,假设我们想要为所有的数组添加一个 last
属性,该属性返回数组的最后一个元素。我们可以在声明文件中为 Array
类型添加一个新的声明:
// types.d.ts
interface Array<T> {
last: T;
}
在上面的代码中,我们通过声明一个同名的 Array
接口来为 Array
类型添加一个新的 last
属性。这样,我们在 TypeScript 代码中使用数组时,就可以访问这个新的 last
属性:
let nums: number[] = [1, 2, 3];
console.log(nums.last); // 3
注意事项
虽然通过声明文件扩展类型定义可以让我们更灵活地使用类型,但也需要注意一些事项。
首先,声明文件只提供类型信息,不包含实现。也就是说,如果我们为一个类型添加了新的属性或方法,我们还需要在实际的代码中提供这些属性或方法的实现。
其次,尽管 TypeScript 允许我们为内置类型添加自定义 的属性和方法,但这并不意味着这是一个好的做法。在很多情况下,过度修改内置类型可能会导致代码难以理解和维护。因此,我们应该谨慎使用这种特性,尽可能地遵循库或语言的原始设计。
最后,当我们在一个项目中使用多个声明文件时,需要注意文件的加载顺序和作用域问题。因为声明文件中的类型声明会影响整个项目,所以我们需要确保所有的声明文件都被正确地加载,并且不会互相冲突。
为第三方库创建声明文件
当我们在使用第三方库时,通常会遇到缺乏类型声明的情况。我们可以通过创建一个声明文件来为该库添加类型声明,以便在 TypeScript 代码中使用该库的时候获得类型检查和自动完成的支持。
以下是一个实际的示例,假设我们使用的是一个名为 axios
的库,它是一个流行的用于发起 HTTP 请求的库。假设 axios
库没有提供类型声明文件,我们可以创建一个声明文件 axios.d.ts
来为它添加类型声明:
declare module 'axios' {
export interface AxiosRequestConfig {
url: string;
method?: string;
data?: any;
headers?: any;
}
export interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
}
export function request<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
export function get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
export function post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
// ... 其他请求方法的类型声明 ...
}
在这个声明文件中,我们使用 declare module
来声明一个名为 axios
的模块,并在其中定义了与 axios
相关的类型声明。
我们定义了 AxiosRequestConfig
接口,它描述了发起请求时的配置选项;定义了 AxiosResponse
接口,它描述了请求返回的响应数据的结构。
然后,我们通过 export
关键字将 request
、get
和 post
等函数导出为模块的公共 API,以便在其他文件中使用这些函数。
现在,在我们的 TypeScript 代码中,我们可以通过导入 axios
模块来使用这些类型声明,以及使用 axios
库的方法:
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
const config: AxiosRequestConfig = {
url: 'https://api.example.com',
method: 'GET',
};
axios.get(config)
.then((response: AxiosResponse) => {
console.log(response.data);
})
.catch((error) => {
console.error(error);
});
通过这种方式,我们可以为第三方库创建声明文件,并在 TypeScript 代码中使用它们来获得类型检查和自动完成的支持,提高代码的可靠性和开发效率。
接口和类
在 TypeScript 中,接口(Interfaces)和类(Classes)是实现面向对象编程(Object-Oriented Programming,OOP)的基础工具。这些工具提供了一种方式来定义和组织复杂的数据结构和行为。
接口
接口在 TypeScript 中扮演着关键的角色,用于强类型系统的支持。接口可以描述对象的形状,使我们可以编写出预期的行为。接口可用于描述对象、函数或者类的公共部分。
以下是一个基本的接口示例:
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
在这个例子中,LabelledValue
接口就像一个名片,告诉其他代码,只要一个对象有label
属性,并且类型为string
,那么就可以认为它是LabelledValue
。
接口也可以描述函数类型:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
此外,接口还能用于描述数组和索引类型:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
interface Dictionary {
[index: string]: string;
}
let myDict: Dictionary;
myDict = { "key": "value" };
类
与传统的 JavaScript 一样,TypeScript 也使用类(Classes)来定义对象的行为。然而,TypeScript 的类具有一些额外的特性,如访问修饰符(Access Modifiers)、静态属性(Static Properties)、抽象类(Abstract Classes)等。
以下是一个基本的类示例:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
TypeScript 还引入了访问修饰符 public
、private
和 protected
。如果没有指定访问修饰符,则默认为 public
。
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
TypeScript 类还支持继承,通过extends
关键字可以创建一个子类。子类可以访问和改变父类的属性和方法:
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
constructor(name: string) { super(name); }
bark() {
console.log('Wo
of! Woof!');
}
}
const dog = new Dog('Tom');
dog.bark();
dog.move(10);
dog.bark();
为了实现多态,TypeScript 提供了抽象类的概念。抽象类是不能被实例化的类,只能作为其他类的基类。抽象类中可以定义抽象方法,抽象方法必须在派生类中实现:
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earth...');
}
}
class Dog extends Animal {
makeSound() {
console.log('Woof! Woof!');
}
}
const myDog = new Dog();
myDog.makeSound();
myDog.move();
枚举和泛型
接下来我们将学习TypeScript 中的两个重要主题:枚举(Enums)和泛型(Generics)。这两个特性能大大提高代码的可重用性和安全性。
枚举
枚举是 TypeScript 中一种特殊的数据类型,允许我们为一组数值设定友好的名字。枚举的定义使用 enum
关键字。
enum Direction {
Up = 1,
Down,
Left,
Right,
}
在这个例子中,我们定义了一个名为 Direction
的枚举,它有四个成员:Up
、Down
、Left
和 Right
。Up
的初始值为 1,其余成员的值会自动递增。
除了使用数值,我们也可以使用字符串:
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
此外,TypeScript 还支持计算的和常量成员。常量枚举通过 const enum
进行定义,TypeScript 会在编译阶段进行优化:
const enum Enum {
A = 1,
B = A * 2
}
异构枚举
TypeScript 支持数字和字符串混用的枚举,这种类型的枚举被称为异构枚举:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
尽管 TypeScript 支持这种用法,但我们在实际项目中应尽可能避免使用异构枚举,因为这会引入不必要的复杂性。
枚举成员的类型
在某些特殊的情况下,枚举成员本身也可以作为一种类型:
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
泛型
在 TypeScript 中,泛型(Generics)是一种强大的类型工具,它允许我们编写可重用、灵活和类型安全的代码。泛型允许我们在定义函数、类或接口时使用类型参数,这些类型参数在使用时可以被动态地指定具体的类型。
以下是泛型在 TypeScript 中的几个常见应用场景:
1. 函数泛型
函数泛型允许我们编写可适用于多种类型的函数,提高代码的重用性和灵活性。例如:
function identity<T>(arg: T): T {
return arg;
}
let result = identity<number>(42); // result的类型为number
在上面的示例中,identity
函数接受一个类型参数T
,表示输入和输出的类型。通过在函数调用时显式指定类型参数为number
,我们可以将42
传递给identity
函数并推断出结果的类型为number
。
2. 接口泛型
接口泛型允许我们创建可适用于不同类型的接口定义。例如:
interface Pair<T, U> {
first: T;
second: U;
}
let pair: Pair<number, string> = { first: 42, second: "hello" };
在上面的示例中,我们定义了一个Pair
接口,它接受两个类型参数T
和U
,表示first
和second
属性的类型。通过指定类型参数为number
和string
,我们创建了一个具体的pair
对象,它的first
属性类型为number
,second
属性类型为string
。
3. 类泛型
类泛型允许我们创建可适用于不同类型的类定义。例如:
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
let container = new Container<number>(42);
let value = container.getValue(); // value的类型为number
在上面的示例中,我们定义了一个Container
类,它接受一个类型参数T
,表示类的内部值的类型。通过在创建类的实例时显式指定类型参数为number
,我们创建了一个具体的container
对象,它的value
属性类型为number
,并可以使用getValue
方法获取该值。
泛型还支持约束(Constraints)的概念,通过使用约束,我们可以限制泛型的类型范围,使其满足特定的条件。
泛型在 TypeScript 中广泛应用于函数、类、接口和类型别名的定义中,它提供了一种灵活、类型安全且可重用的方式来处理不同类型的数据。通过使用泛型,我们可以在编写代码时提供更强大的类型支持,从而减少错误并提高代码的可维护性和可读性。
泛型和类型体操
泛型和类型体操(Type Gymnastics)是 TypeScript 中高级类型系统的重要组成部分。它们提供了强大的工具和技巧,用于处理复杂的类型操作和转换。
泛型(Generics)
1. 泛型函数
泛型函数允许我们在函数定义中使用类型参数,以便在函数调用时动态指定类型。例如:
function identity<T>(arg: T): T {
return arg;
}
let result = identity<number>(42); // result 的类型为 number
在上面的示例中,identity
函数使用类型参数 T
,并返回与输入类型相同的值。通过显式传递泛型参数,我们可以确保在函数调用时指定了具体的类型。
2. 泛型接口
泛型接口允许我们在接口定义中使用类型参数,以便在实现该接口时指定具体的类型。例如:
interface Container<T> {
value: T;
}
let container: Container<number> = { value: 42 };
在上面的示例中,我们定义了一个泛型接口 Container
,它包含一个类型参数 T
。通过指定 Container<number>
,我们创建了一个具体的实现,其中的 value
属性类型为 number
。
3. 泛型类
泛型类允许我们在类定义中使用类型参数,以便在创建类的实例时指定具体的类型。例如:
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
let stack = new Stack<number>();
stack.push(1);
stack.push(2);
let item = stack.pop(); // item 的类型为 number | undefined
在上面的示例中,我们定义了一个泛型类 Stack
,它使用类型参数 T
来表示堆栈中的元素类型。通过创建 Stack<number>
的实例,我们限制了堆栈中的元素必须为 number
类型。
类型体操(Type Gymnastics)
1. 条件类型(Conditional Types)
条件类型允许我们根据输入类型的条件判断结果来选择不同的类型。条件类型的语法形式为:
T extends U ? X : Y
其中,T
是待检查的类型,U
是条件类型,X
是满足条件时返回的类型,Y
是不满足条件时返回的类型。
下面是一个使用条件类型的示例:
type Check<T> = T extends string ? true : false;
type Result = Check<string>; // Result 的类型为 true
在上面的示例中,我们定义了一个条件
类型 Check<T>
,它接受一个类型参数 T
。如果 T
是 string
类型,那么 Check<T>
的类型将是 true
,否则为 false
。
2. keyof
操作符和索引访问类型
keyof
操作符用于获取类型的所有属性名,结合索引访问类型可以从一个类型中获取属性的具体类型。
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // "name" | "age"
type PersonNameType = Person['name']; // string
在上面的示例中,我们使用 keyof
操作符获取了 Person
接口的属性名集合,并通过索引访问类型获取了 Person
接口中 name
属性的类型 。
3. infer
关键字
infer
关键字用于在条件类型中推断类型,并将其赋值给一个类型变量。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnValue = ReturnType<typeof add>; // 类型为 number
在上面的示例中,ReturnType
类型接受一个类型参数 T
,并使用条件类型和 infer
关键字推断函数类型的返回类型。通过调用 ReturnType<typeof add>
,我们推断出 add
函数的返回类型为 number
。
当涉及到泛型时,还有一些重要的概念和内置泛型函数可以深入分析。让我们继续探讨 extends
关键字、TS 官方内置的一些泛型函数以及它们的使用。
extends
关键字和类型约束
在泛型中,我们可以使用 extends
关键字来对泛型类型进行约束。这样可以确保传递给泛型的类型满足特定条件。
function printProperty<T extends { name: string }>(obj: T): void {
console.log(obj.name);
}
printProperty({ name: 'John', age: 25 }); // 输出 'John'
在上面的示例中,printProperty
函数接受一个泛型参数 T
,该参数必须满足一个约束条件:具有 name
属性,且 name
的类型为 string
。通过使用 extends
关键字和类型约束,我们可以确保 obj
参数具有所需的属性和类型,从而避免出现错误。
泛型函数Util
TypeScript 提供了一些内置的泛型函数,这些函数被广泛用于处理各种类型操作。以下是一些常见的官方内置泛型函数:
Partial<T>
Partial<T>
是 TypeScript 中的一个内置泛型类型,它可以将给定类型 T
中的所有属性转换为可选属性。这对于创建部分完整的对象非常有用。
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: 'John' }; // age 属性是可选的
在上面的示例中,Partial<Person>
将 Person
接口中的所有属性变为可选属性,从而创建了一个部分完整的 PartialPerson
类型。
Required<T>
Required<T>
是 TypeScript 中的另一个内置泛型类型,它可以将给定类型 T
中的所有可选属性转换为必需属性。这对于确保对象的完整性非常有用。
interface Person {
name?: string;
age?: number;
}
type RequiredPerson = Required<Person>;
const requiredPerson: RequiredPerson = { name: 'John', age: 25 }; // name 和 age 属性是必需的
在上面的示例中,Required<Person>
将 Person
接口中的所有可选属性变为必需属性,从而创建了一个要求完整性的 RequiredPerson
类型。
Pick<T, K>
Pick<T, K>
是 TypeScript 中的另一个内置泛型函数,它可以从给定类型 T
中选择指定的属性 K
组成一个新的类型。
interface Person {
name: string;
age: number;
address: string;
}
type NameAndAge = Pick<Person, 'name' | 'age'>;
const person: NameAndAge = { name:
'John', age: 25 }; // 只包含 name 和 age 属性
在上面的示例中,Pick<Person, 'name' | 'age'>
从 Person
接口中选择了 'name'
和 'age'
属性,创建了一个新的类型 NameAndAge
。
我们还可以结合泛型和内置泛型函数来实现更复杂的类型操作。以下是一个示例,展示了如何使用 Pick
和泛型来创建一个函数,该函数从给定对象中选择指定属性,并返回一个新的对象。
function pickProperties<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result: Partial<T> = {};
for (const key of keys) {
result[key] = obj[key];
}
return result as Pick<T, K>;
}
interface Person {
name: string;
age: number;
address: string;
}
const person: Person = {
name: 'John',
age: 25,
address: '123 Main St'
};
const nameAndAge = pickProperties(person, ['name', 'age']); // 只包含 name 和 age 属性
console.log(nameAndAge); // 输出: { name: 'John', age: 25 }
在上面的示例中,pickProperties
函 数接受一个泛型参数 T
和一个属性数组 keys
。通过使用 Pick<T, K>
,我们将从给定对象 obj
中选择指定的属性 keys
,并创建一个新的对象。
这个例子结合了泛型、内置泛型函数 Pick
、keyof
操作符和 extends
关键字,展示了如何在 TypeScript 中处理复杂的类型操作和转换。
当涉及到官方内置的泛型函数时,还有一些重要的函数值得分析。让我们继续探讨一些常用的官方内置泛型函数以及它们的使用。
Exclude<T, U>
Exclude<T, U>
是 TypeScript 中的一个内置泛型函数,用于从类型 T
中排除类型 U
。它返回一个新类型,该新类型包含在 T
中存在但不在 U
中存在的成员类型。
type T = Exclude<"a" | "b" | "c", "a" | "b">; // T 的类型为 "c"
在上面的示例中,Exclude<"a" | "b" | "c", "a" | "b">
排除了类型 "a"
和 "b"
,返回类型为 "c"
。
Omit<T, K>
Omit<T, K>
是 TypeScript 中的另一个内置泛型函数,它返回一个新类型,该新类型排除了类型 T
中指定的属性 K
。
interface Person {
name: string;
age: number;
address: string;
}
type PersonWithoutAddress = Omit<Person, "address">;
在上面的示例中,Omit<Person, "address">
返回了一个新类型 PersonWithoutAddress
,该类型排除了 Person
接口中的 address
属性。
Readonly<T>
Readonly<T>
是 TypeScript 中的另一个内置泛型函数,它将类型 T
中的所有属性转换为只读属性。
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
在上面的示例中,Readonly<Person>
将 Person
接口中的所有属性变为只读属性,创建了一个新类型 ReadonlyPerson
。
总结
泛型和类型体操是 TypeScript 中强大的类型系统的关键组成部分。通过使用泛型,我们可以创建可重用、灵活和类型安全的代码。内置泛型函数提供了一些常用的类型转换工具,如 Partial
、Required
和 Pick
,可以帮助我们更方便地处理类型操作。
通过结合泛型、extends
关键字、内置泛型函数和其他高级类型概念,我们能够在 TypeScript 中编写更复杂、类型安全的代码,并利用 TypeScript 的强大类型系统来提高代码的可读性、可维护性和可扩展性。
类型
TypeScript提供了JavaScript的所有基本数据类型,如:number
、string
、boolean
等。它还增加了额外的类型,比如any
、unknown
、never
、void
等。
number
在TypeScript中,所有的数字都是浮点数。这些数字的类型是number
。下面是一些例子:
let decimal: number = 6; // 十进制
let hex: number = 0xf00d; // 十六进制
let binary: number = 0b1010; // 二进制
let octal: number = 0o744; // 八进制
string
string
类型表示文本数据。你可以使用单引号(')或双引号(")定义字符串,也可以使用反引号(`)定义模板字符串:
let color: string = "blue";
color = 'red';
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }. I'll be ${ age + 1 } years old next month.`;
boolean
boolean
类型有两个值:true
和false
:
let isDone: boolean = false;
Array
在TypeScript中,数组类型有两种表达方式。一种是在元素类型后面加上 []
,表示由此类型元素组成的一个数组。另一种方式是使用数组泛型,Array<元素类型>:
let list: number[] = [1, 2, 3];
// 或
let list: Array<number> = [1, 2, 3];
Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。比如,你可以定义一对值分别为 string
和number
的元组:
let x: [string, number];
x = ['hello', 10]; // OK
以上是TypeScript的一些基本类型。在接下来的对话中,我们可以进一步讨论其他的TypeScript类型,比如枚举(enum
)、null
、undefined
、never
、void
以及对象类型。
Enum
Enum是一种特殊的类型,它可以更容易地处理一组有名字的常量。在TypeScript中,enum的默认起始值是0,然后每个成员的值都会依次增加。你也可以手动为enum的成员指定值:
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
// 手动指定成员的数值
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
Null and Undefined
在TypeScript中,undefined
和null
各自有自己的类型,分别是undefined
和null
。默认情况下,它们是所有类型的子类型。这意味着你可以把 null
和undefined
赋值给 number
类型的变量。
然而,当你指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自的类型:
let u: undefined = undefined;
let n: null = null;