一、type 和 interface 的区别
在 TypeScript 里,type(类型别名)和 interface(接口)都可用于定义对象类型。
1. 定义语法
type:使用type关键字来创建类型别名,它能定义各种类型,像基本类型、联合类型、交叉类型、元组类型等。
// 基本类型别名
type MyString = string;
// 联合类型
type StringOrNumber = string | number;
// 对象类型
type Person = {
name: string;
age: number;
};
interface:运用interface关键字定义接口,主要用于定义对象的结构。
interface Person {
name: string;
age: number;
}
2. 扩展方式
type:借助交叉类型(&)来实现扩展。
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
const myDog: Dog = {
name: 'Buddy',
breed: 'Golden Retriever'
};
interface:使用extends关键字进行扩展。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const myDog: Dog = {
name: 'Buddy',
breed: 'Golden Retriever'
};
3. 重复定义
type:不能重复定义相同名称的类型别名,重复定义会引发编译错误。
type Point = {
x: number;
y: number;
};
// 下面这行代码会报错
// type Point = {
// z: number;
// };
interface:可以重复定义相同名称的接口,这些定义会自动合并。
interface User {
name: string;
}
interface User {
age: number;
}
const user: User = {
name: 'John',
age: 30
};
4. 对基本类型的支持
type:能够定义基本类型的别名,还可以定义联合类型、交叉类型、元组类型等复杂类型。
// 基本类型别名
type MyNumber = number;
// 联合类型
type StringOrBoolean = string | boolean;
// 元组类型
type Tuple = [string, number];
interface:主要用于定义对象类型,无法直接定义基本类型、联合类型或元组类型。不过可以通过定义对象属性的方式间接支持部分复杂类型。
// 无法直接定义联合类型,但可以这样使用
interface UnionWrapper {
value: string | number;
}
5. 实现方式
type:类不能直接实现类型别名,不过可以通过交叉类型或联合类型组合多个类型别名来实现。
type Printable = {
print: () => void;
};
class MyClass implements Printable {
print() {
console.log('Printing...');
}
}
interface:类可以直接实现接口,实现接口时,类必须包含接口中定义的所有属性和方法。
interface Printable {
print: () => void;
}
class MyClass implements Printable {
print() {
console.log('Printing...');
}
}
6. 映射类型支持
type:在定义映射类型时更常用,因为它可以直接使用类型操作符来创建新类型。
type ReadonlyPerson = {
readonly [P in keyof Person]: Person[P];
};
interface:在映射类型方面的支持不如type灵活,通常使用type来实现映射类型。
二、declare 关键字的作用
declare 关键字主要用于告诉编译器某个变量、函数、类、接口或者模块等的类型信息,而无需提供具体的实现。它在处理外部库、全局变量和旧代码时非常有用,能让 TypeScript 编译器理解这些外部资源的类型,从而进行类型检查。
1. 声明全局变量
当你使用一些全局变量(例如在 HTML 文件里通过 <script> 标签引入的库所创建的全局变量)时,TypeScript 编译器或许不了解这些变量。此时可以用 declare 来声明这些变量的类型。
// 声明一个全局变量 jQuery
declare const jQuery: (selector: string) => any;
// 使用全局变量
const $ = jQuery('body');
2. 声明函数
若要使用外部定义的函数,但是不想在 TypeScript 里实现它,就可以使用 declare 声明函数的签名。
// 声明一个全局函数
declare function greet(name: string): void;
// 使用声明的函数
greet('John');
3. 声明类
在引用外部类时,若不想实现这个类,可使用 declare 声明类的结构。
// 声明一个类
declare class Person {
constructor(name: string, age: number);
getName(): string;
getAge(): number;
}
// 使用声明的类
const person = new Person('Alice', 25);
console.log(person.getName());
4. 声明接口
在 TypeScript 中,接口常用来定义对象的形状。使用 declare 可以声明全局接口。
// 声明一个接口
declare interface User {
name: string;
age: number;
}
// 使用声明的接口
const user: User = { name: 'Bob', age: 30 };
5. 声明模块
要是使用外部模块,却不想引入其具体实现,就可以使用 declare 声明模块。
// 声明一个模块
declare module 'lodash' {
export function cloneDeep<T>(value: T): T;
}
// 使用声明的模块
import { cloneDeep } from 'lodash';
const original = { a: 1, b: { c: 2 } };
const cloned = cloneDeep(original);
6. 声明命名空间
在旧版本的 TypeScript 里,命名空间用于组织代码。使用 declare 可以声明全局命名空间。
// 声明一个命名空间
declare namespace MyNamespace {
export function doSomething(): void;
}
// 使用声明的命名空间
MyNamespace.doSomething();
三、泛型及其作用
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而是在使用的时候再指定类型的一种特性。通过泛型,你可以编写通用的代码,而不是为每一种具体的类型都编写一套代码。
使用场景
- 函数:创建可处理多种数据类型的函数。
- 类:构建可操作不同数据类型的类。
- 接口:定义能适配多种类型的接口。
语法和示例
泛型函数
下面是一个简单的泛型函数示例,该函数用于返回传入的参数:
function identity<T>(arg: T): T {
return arg;
}
// 使用泛型函数
let output1 = identity<string>("myString");
let output2 = identity<number>(100);
console.log(output1);
console.log(output2);
在上述代码中,<T> 是泛型类型变量,它代表一种类型。在调用 identity 函数时,我们可以通过 <string> 或 <number> 来指定 T 的具体类型。
泛型类
以下是一个泛型类的示例,该类表示一个简单的栈结构:
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
// 使用泛型类
let numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop());
let stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.pop());
在这个例子中,Stack 类使用了泛型 T,可以存储任意类型的数据。在创建 Stack 实例时,通过 <number> 或 <string> 来指定存储的数据类型。
泛型接口
下面是一个泛型接口的示例,该接口定义了一个函数类型:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(10));
在这个示例中,GenericIdentityFn 是一个泛型接口,它定义了一个函数类型,该函数接收一个类型为 T 的参数并返回一个类型为 T 的值。
泛型约束
有时候,你可能希望对泛型类型进行一些限制,这时可以使用泛型约束。例如,你希望泛型类型必须具有某个属性:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity("hello");
// loggingIdentity(10); // 报错,因为 number 类型没有 length 属性
在上述代码中,T extends Lengthwise 表示 T 必须是 Lengthwise 接口的子类型,即 T 类型必须具有 length 属性。