TypeScript 接口(interface)

在使用 JavaScript 开发时,对象(Object)是我们每天打交道最多的数据结构之一。JavaScript 中的对象非常自由,我们可以随时往一个对象添加属性或删除属性。

但是,这种 “极度自由” 在大型企业级项目中往往是致命的。假设我们从后端接口拿到了一个包含用户信息的对象,然后在代码中不小心把 userName 写成了 username,或者漏传了某个必填字段,此时 JavaScript 是不会在编写阶段给你任何警告的,它会默默吞下错误,直到程序运行崩溃。

为了对 “对象” 这种数据结构进行规范约束,使得错误在代码编写阶段就能暴露出来,TypeScript 引入了一个非常重要的概念:接口(interface)

TypeScript 接口是什么?

在 TypeScript 中,接口(interface)本质上是一份 “契约”。它是专门用来约束对象应该长什么样,官方称之为对象的 “形状(Shape)”。

只要一个对象声明了遵守某个接口,那么该对象就必须严格遵守 “契约” 上的规定:即不能多一个属性,也不能少一个属性,并且每个属性的数据类型都要一一对应。

语法:

interface 接口名 {
    属性名1: 类型1;
    属性名2: 类型2;
    ...
}

说明:

需要注意的是,在 TypeScript 的规范中,接口名的首字母一般推荐使用大写(即大驼峰命名法),比如 User、Product 等。

示例 1:定义接口

// 定义接口
interface User {
    name: string;
    age: number;
}

// 使用接口(用于创建对象)
const currentUser: User = {
    name: "Jack",
    age: 20
};

console.log(currentUser.name);

运行结果如下。

Jack

分析:

在这个例子中,我们定义了一个 User 接口,它规定了对象必须包含 name(字符串)和 age(数字)。

接口就像是一份 “契约”,如果我们试图挑战这份契约,比如少写一个 age,或者多写一个 gender,那么 TypeScript 编译器立马就会 “翻脸” 飘红报错,比如:

// 定义接口
interface User {
    name: string;
    age: number;
}

// 错误写法 1:少了 age 属性
const user1: User = {
    name: "Jack" 
}; 
// 报错:类型 "{ name: string; }" 中缺少属性 "age"

// 错误写法 2:多了 gender 属性
const user2: User = {
    name: "Lucy",
    age: 18,
    gender: "女"
}; 
// 报错:对象文字可以只指定已知属性,并且 “gender” 不在类型 “User” 中

TypeScript 接口的 “可选属性”

在真实的业务开发中,对象的属性很多时候并不都是必填的。比如在用户注册时,用户的 “昵称” 和 “密码” 是必填的,但像 “兴趣爱好(hobby)” 这种就是选填的。

为了应对这种场景,TypeScript 接口提供了 “可选属性” 的语法。

语法:

interface 接口名 {
    属性名1 ?: 类型1;
    属性名2 ?: 类型2;
    ...
}

说明:

我们只需要在属性名的后面加上一个问号 “?” 即可,该属性就会变成 “可选属性”。

示例 2:使用可选属性

interface User {
    name: string;
    age: number;
    hobby?: string;    // 这是一个可选属性
}

// 包含可选属性
const user1: User = { 
    name: "Jack", 
    age: 20, 
    hobby: "Coding" 
};

// 不包含可选属性,同样完全合法
const user2: User = { 
    name: "Jack", 
    age: 25 
};

console.log(user1.hobby);
console.log(user2.hobby);

运行结果如下。

Coding
undefined

分析:

可选属性的好处在于:它既对有可能存在的属性进行了类型限制,又赋予了对象一定的灵活性。

对于这个例子来说,如果 user2 没写 hobby,此时它是合法的;但如果 user2 写了 hobby,那么它的值就必须是 string 类型,我们绝对不能塞个数字进去。

TypeScript 接口的 “只读属性”

有些对象的属性一旦被创建,在它整个生命周期内都不允许被修改。最典型的例子就是数据库中的自增 id,或者是用户的身份证号。

在 TypeScript 接口中,我们可以在属性名前面加上 readonly 关键字,将其标记为只读属性。

语法:

interface 接口名 {
    readonly 属性名1: 类型1;
    readonly属性名2: 类型2;
    ...
}

说明:

readonly 是放在属性名的 “前面”,而可选属性符号 “?” 是放在属性名 “后面”。

示例 3:使用只读属性

interface Product {
    readonly id: number; // 只读属性
    title: string;
    price: number;
}

const myPhone: Product = { 
    id: 1001, 
    title: "智能手机", 
    price: 4999 
};

// 正常操作:修改普通属性
myPhone.price = 3999;

// 错误操作:试图修改只读属性
myPhone.id = 1002;
// 报错:无法为 “id” 赋值,因为它是只读属性

const 与 readonly 的区别

初学的小伙伴很容易把 readonly 和 const 搞混,我们只需要记住一个口诀就可以了:变量用 const,属性用 readonly。

  • const:用来限制整个变量的内存地址不能改变。
  • readonly:是在接口中使用的,专门用来限制对象内部的某个具体属性不能被重新赋值。

TypeScript 接口的 “任意属性(索引签名)”

在实际开发中,偶尔会遇到一种非常动态的数据结构:我们只知道这个对象里全都是某种类型的值,但事先并不知道具体的属性名(key)会叫什么。

比如,后端返回了一个记录页面访问量的配置对象,里面的 key 是动态的页面路径,value 是具体的访问数字。对于这种 “未知名称” 的属性,我们需要使用 TypeScript 的 “索引签名(Index Signatures)”。

语法:

interface 接口名 {
    [key: string]: 类型;
}

示例 4:使用任意属性

interface PageViews {
    // 必须包含一个总数
    total: number;
    // 允许有任意数量的、名字是字符串、值是数字的额外属性
    [key: string]: number;
}

const views: PageViews = {
    total: 1005,
    "/home": 500,
    "/about": 300,
    "/contact": 205
};

console.log(views["/home"]);

运行结果如下。

500

分析:

在这个例子中,[key: string]: number 用于告诉 TypeScript:除了上面明确规定的 total 之外,这个对象还可以接受无数个额外属性,只要属性名是字符串、值是数字即可。

TypeScript 接口的 “业务场景”

学到这里,小伙伴们肯定会问:“接口在实际项目中到底应该怎么用呢?”

实际上,接口最最经典的应用场景就是 “约束函数的参数” 和 “约束 API 请求的返回值”。当函数的参数是一个拥有众多属性的复杂对象时,使用接口能让代码变得极具可读性和安全性。

示例 5:使用接口约束 “函数参数”

// 定义文章接口
interface Article {
    title: string;
    author: string;
    isPublished: boolean;
}

// 使用接口来约束函数参数
function printArticleInfo(article: Article) {
    console.log(`《${article.title}》 - ${article.author}`);
}

const newArticle: Article = {
    title: "TypeScript 接口",
    author: "Jack",
    isPublished: true
};

// 调用函数
printArticleInfo(newArticle);

运行结果如下。

TypeScript 接口》 - Jack

分析:

在 Vue 或 React 的大型项目中,如果我们向某个组件传递复杂的 props,或者封装 Axios 的时候,绝大部分情况都是先抽离定义成一个 interface,然后再使用这个 interface 去约束数据。这也就是为什么大家常说:“面向接口编程” 是 TypeScript 的核心灵魂

示例 6:使用接口约束 “API 返回值”

// 定义接口
interface ApiResponse {
    code: number;
    msg: string;
    data: {
        id: number;
        username: string;
        avatar?: string; // 头像是可选的
    };
}

// 模拟从后端获取到的真实数据
const res: ApiResponse = {
    code: 200,
    msg: "获取用户信息成功",
    data: {
        id: 1001,
        username: "Jack"
    }
};

// 在页面中安全地使用这些数据
console.log(res.data.username);

运行结果如下。

Jack

分析:

在前后端分离的开发中,前端最常做的事情就是调用后端 API 获取数据。后端返回的数据往往是嵌套的复杂 JSON 对象。为了防止在页面渲染时因为 “字段名写错” 而导致白屏,我们通常会使用接口把返回的数据结构严格定义出来。

在这个例子中,如果没有使用 ApiResponse 这个接口来约束,当我们敲下 res.data. 的时候,编辑器是完全不知道里面有什么数据的。我们只能靠死记硬背或者去翻后端的 API 文档,如果一不小心把 username 写成了 userName,此时 Bug 就诞生了。

但是有了接口的约束,当我们在 VS Code 中敲下 res.data. 的那一刻,编辑器会自动弹出 id、username 和 avatar 的智能提示!这不仅极大地提高了我们的开发效率,还相当于给我们的代码上了一份 “保险”,让不可控的后端数据变得完全可控。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号