TypeScript(TS)中对象类型定义的几种方式

定义方式

在 TypeScript 中,以下几种方式用于定义对象:

接口(Interface)

常用场景:

接口用于定义对象的结构,尤其是当对象结构比较复杂、需要复用或者要用于类的类型定义时。

接口非常适合用于定义 API 的数据结构或者复杂的对象类型。

示例:

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age: number;
isActive: boolean;
}

const person: Person = {
name: "Alice",
age: 30,
isActive: true
};

类型别名(Type Alias)

常用场景:

类型别名可以定义对象类型,也可以定义联合类型、交叉类型等。

它非常灵活,适用于定义各种复杂类型,包括对象类型。

示例:

1
2
3
4
5
6
7
8
9
10
11
type Person = {
name: string;
age: number;
isActive: boolean;
};

const person: Person = {
name: "Alice",
age: 30,
isActive: true
};

类(Class)

常用场景:

类用于定义具有特定行为和属性的对象。它适用于面向对象编程,尤其是在需要创建多个具有相同结构和行为的对象实例时。

示例:

1
2
3
4
5
6
7
8
9
class Person {
constructor(
public name: string,
public age: number,
public isActive: boolean
) {}
}

const person = new Person("Alice", 30, true);

对象字面量(Object Literal)

常用场景:

对象字面量通常用于简单的数据结构或者临时使用的对象。

对于复杂对象结构或需要复用的类型定义,不建议使用这种方式。

示例:

1
2
3
4
5
const person: { name: string; age: number; isActive: boolean } = {
name: "Alice",
age: 30,
isActive: true
};

使用场景

  • 接口(Interface)类型别名(Type Alias) 是最常用的定义对象类型的方式,尤其是在大型应用程序或库中。

    接口在扩展和复用方面有优势,而类型别名更为灵活,适合定义复杂的联合类型和交叉类型。

  • 类(Class) 在需要封装对象行为时使用较多,例如在面向对象编程中创建多个实例时。

    它提供了更多的功能,如构造函数、方法和继承。

  • 对象字面量(Object Literal) 适用于简单场景,通常在局部变量或临时对象的定义中使用较多。

总体来说,接口和类型别名是最常见的选择,特别是在 TypeScript 的类型系统中,它们提供了最好的类型安全和灵活性。

类型定义文件

在 TypeScript 中,如果你有一个类型定义文件(通常是以 .d.ts 为扩展名的文件)。

个人建议类型定义都放在src/assets/types/文件夹下。

全局使用

注意

命名要防止和其他地方冲突。

如果你的类型定义文件不使用模块系统,而是定义了全局类型,你可以直接在 TypeScript 项目的 tsconfig.json 文件中通过 include 来包含这些类型定义文件。

TypeScript 编译器会自动识别并将其应用于整个项目。

我这里把全局使用的类型定义都放在了src/assets/types/下。

全局定义的名称不能重复,全局定义的类型不需要导出(export)

1
2
3
4
interface ZUser {
name: string;
age: number;
}

我的项目是引用了tsconfig.app.json,所以我在该文件中添加:

1
2
3
{
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/assets/types/*.d.ts"],
}

配置过后,可能需要重启IDEA才能生效。

引用方式使用

user.d.ts

1
2
3
4
export interface User {
name: string;
age: number;
}

使用

1
2
3
4
5
6
import type { ZUser } from '@/assets/types/user'

const person: User = {
name: 'Alice',
age: 30
};

可选属性和可空属性

  • 可选属性:使用问号 ?,表示属性可能存在也可能不存在。
  • 可空属性:在属性类型中添加 nullundefined,表示属性可以是指定类型,也可以是 nullundefined

可选属性

1
2
3
4
5
6
7
8
9
10
11
12
13
interface User {
name: string;
age?: number; // 可选属性
}

let user1: User = {
name: "Alice"
}; // 合法,因为 age 是可选的

let user2: User = {
name: "Bob",
age: 30
}; // 也合法,因为 age 是可选的

可空属性

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string;
age: number|null;
isActive: boolean;
}

const person: Person = {
name: "Alice",
age: null,
isActive: true
};

类型断言

类型断言

类型断言允许开发者手动指定一个值的类型,有两种语法形式:

  • 尖括号语法:<类型>值
  • as语法:值 as 类型
1
2
3
4
5
// 类型断言
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
// 或者使用尖括号语法
let strLength2: number = (<string>someValue).length;

泛型

无泛型

不使用泛型时

1
2
3
4
5
interface ZResult {
code: number
msg: string
obj: any
}

单泛型

1
2
3
4
5
6
7
8
9
10
11
12
interface ZResult<T> {
code: number
msg: string
obj: T
}

// 定义一个具体的类型
interface User {
id: number
name: string
email: string
}

这时候可以这样用

1
2
3
4
5
6
7
8
9
10
11
12
// 使用ZResult接口并指定obj的类型为User
const userResult: ZResult<User> = {
code: 200,
msg: "User fetched successfully",
obj: {
id: 1,
name: "John Doe",
email: "john.doe@example.com"
}
};

console.log(userResult);

多泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface ZResult<T, U> {
code: number
msg: string
obj: T
extraInfo?: U
}

// 定义两个具体的类型
interface User {
id: number
name: string
email: string
}

interface ExtraInfo {
timestamp: Date
source: string
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用ZResult接口并指定obj的类型为User,extraInfo的类型为ExtraInfo
const userResult: ZResult<User, ExtraInfo> = {
code: 200,
msg: "User fetched successfully",
obj: {
id: 1,
name: "John Doe",
email: "john.doe@example.com"
},
extraInfo: {
timestamp: new Date(),
source: "database"
}
};

console.log(userResult);

扩展接口属性

window是全局唯一的对象,在JS中我们可以方便的添加任何我们想全局访问的属性,但是在TS中不行。

我们需要扩展 Window 接口

添加一个全局的定义文件 globals.d.ts

1
2
3
interface Window {
loginUser?: ZLoginUser
}

这样我们就可以在TS中使用了

1
window.loginUser = {name:"张三"}

添加属性

在 TypeScript 中,如果你需要在现有类型中添加或修改属性,有几种常见的方法可以实现。这些方法包括扩展接口、使用类型交叉、和声明合并。

下面详细介绍这些方法:

接口扩展

如果你有一个已有的接口或类型,想要在其基础上添加新属性,可以通过继承或扩展接口来实现。这通常通过声明合并来完成。

示例:

假设你有一个基础接口 Person,现在你想要在其基础上添加一个 age 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基础接口
interface Person {
name: string;
}

// 扩展接口
interface Person {
age?: number; // 可选属性
}

// 使用扩展后的接口
const person: Person = {
name: "Alice",
age: 30
};

使用类型交叉

类型交叉允许你创建一个新类型,它结合了多个类型的属性。这种方式可以用于组合现有类型,或者在类型定义中添加新的属性。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 基础类型
interface Person {
name: string;
}

// 新增属性
interface AdditionalInfo {
age?: number;
}

// 通过交叉类型组合两个接口
type ExtendedPerson = Person & AdditionalInfo;

// 使用扩展后的类型
const person: ExtendedPerson = {
name: "Bob",
age: 25
};

类型别名与交叉类型

除了使用接口,你也可以使用类型别名和交叉类型来实现类似的效果。这在处理复杂类型或需要快速组合类型时特别有用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 基础类型
type Person = {
name: string;
};

// 新增属性
type AdditionalInfo = {
age?: number;
};

// 通过交叉类型组合两个类型
type ExtendedPerson = Person & AdditionalInfo;

// 使用扩展后的类型
const person: ExtendedPerson = {
name: "Charlie",
age: 28
};

模块扩展

如果你需要扩展模块的类型(例如,window 对象),可以在模块扩展中定义新的属性。

示例:

1
2
3
4
5
6
// globals.d.ts
declare module "global" {
interface Window {
myCustomProperty?: string; // 可选属性
}
}

然后在代码中使用扩展后的 window 属性:

1
2
3
// 在 TypeScript 文件中
window.myCustomProperty = "Hello, TypeScript!";
console.log(window.myCustomProperty); // 输出 "Hello, TypeScript!"

在类中添加属性

如果你正在处理类类型,可以直接在类中添加新属性或方法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
name: string;

constructor(name: string) {
this.name = name;
}
}

// 扩展类
class ExtendedPerson extends Person {
age?: number;

constructor(name: string, age?: number) {
super(name);
this.age = age;
}
}

const person = new ExtendedPerson("David", 40);
console.log(person.name); // 输出 "David"
console.log(person.age); // 输出 40

总结

  • 接口扩展:使用声明合并来扩展已有接口。
  • 类型交叉:使用交叉类型 (&) 来组合多个类型。
  • 模块扩展:扩展模块的类型以添加新属性。
  • 类扩展:在类中添加新的属性或方法。