我们研发开源了一款基于 Git 进行技术实战教程写作的工具,我们图雀社区的所有教程都是用这款工具写作而成,欢迎 Star 哦
如果你想快速了解如何使用,欢迎阅读我们的 教程文档 哦
学习了注解函数,又了解了类型运算如联合类型和交叉类型,接下来我们来了解一些 TS 中独有的类型别名,它类似 JS 变量,是类型变量,接着我们还会学习 TS 中内容非常庞杂的内容之一:类,了解 TS 中类的独有特性,以及如何注解类,甚至用类去注解其他内容。
欢迎阅读 类型即正义,TypeScript 从入门到实践系列:
- 《类型即正义:TypeScript 从入门到实践(序章)》:用 TS 初始化一个 React 项目
- 《类型即正义:TypeScript 从入门到实践(一)》:讲解基础的 TS 类型和接口
- 《类型即正义:TypeScript 从入门到实践(二)》:讲解如何注解函数、高级类型以及类型守卫
本文所涉及的源代码都放在了 Github 或者 Gitee 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github 或 Gitee仓库加星❤️哦~
此教程属于 React 前端工程师学习路线的一部分,欢迎来 Star 一波,鼓励我们继续创作出更好的教程,持续更新中~
运行代码
如果你偏爱 码云,那么你可以运行如下命令获取这一步的代码,然后你可以跟着文章的内容将代码做出修改:
git clone -b part-three https://gitee.com/tuture/typescript-tea.git
cd typescript-tea && npm install && npm start
如果你偏爱 Github,那么你可以运行如下命令来获取初始代码:
git clone -b part-thre git@github.com:tuture-dev/typescript-tea.git
cd typescript-tea && npm install && npm start
类型别名
就像我们为了在平时开发中更加灵活而创建变量或者干掉硬编码数据一样,TS 为我们提供了类型别名,它允许你为类型创建一个名字,这个名字就是类型的别名,进而你可以在多处使用这个别名,并且有必要的时候,你可以更改别名的值(类型),以达到一次替换,多处应用的效果。
我们来看一个简单的类型别名的例子,假如我们有一个获取一个人姓名的函数,它接收一个参数,这个参数有可能直接是要获取的姓名,它是一个 string
类型,也有可能是一个另外一个函数,需要调用它以获取姓名,它是一个函数类型,我们来看一下这个例子:
function getName(n) {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}
如果我们要给这个 n
进行类型注解,那么它应该同时是 string | () => string
,是 string
类型和 () => string
函数类型的联合类型,有过一定开发经验的同学可能会发觉,这样写可能很影响原代码的可读性,而且这个 n
的类型可能会变化,因为我们的函数可能扩展,所以如果我们用一个类型别名把这个 n
的类型表示出来,那么就类似我们用变量替代了硬编码,可扩展性就更强了,我们马上来尝试一下:
type NameParams = 'string' | () => 'string';
function getName(n: NameParams): string {
// ... 其它一样
}
可以看到我们用了一个 NameParams
类型别名,它保存着原联合类型,类型别名就是等号左边是 type
关键字加上别名变量,等号右边是带保存的类型,这个类型很广,它可以是字面量类型,基础类型,元组、函数、联合类型和交叉类型、甚至还可以是其他类型别名的组合。
所以对于上面的 NameParams
,我们可以进一步拆解它为如下的样子:
type Name = string;
type NameResolver = () => string;
type NameParams = Name | NameResolver;
function getName(n: NameParams): Name {
// ... 其他一样
}
我们看到,上面这个不仅更加细粒度,我们将 NameParams
拆成了两个类型别名:Name
和 NameResolver
,分别处理 string
和 () => string
的情况,然后通过联合操作符联合赋值给 NameParams
;还带来了一个优势,我们的返回值可以更加明确就是 Name
类型,这样 Name
变化,它可能变成 number
类型,那么也能很好的反应这个变化,且只需要修改一下 Name
的值为 number
类型就可以了,所有其他的 Name
类型会自动变化。
类型别名与接口
有同学读到这里,可能有疑问了,这个类型别名貌似无所不能嘛,那它和接口有什么区别了?
接口主要是用来定义一个结构的类型,比如定义一个对象的类型,而类型别名可以是任意细粒度的类型定义,比如我们前面讲的最原子的字母量类型如 'hello tuture'
类型,到对象类型如:
type tuture = {
tutureCommunity: string;
editure: string;
tutureDocs: string;
}
上面这个类型我们定义了一个包含三个属性的对象类型,并用 tuture
别名来存储它们。
定义上面这个对象的类型我们可以用之前学到的接口这样写:
interface Tuture {
tutureCommunity: string;
editure: string;
tutureDocs: string;
}
可以看到类型别名既可以表达接口所表达的类型,还比接口更加细粒度,它还可以是一个基础类型如 type name = 'string'
。
动手实践
还记得之前我们那个 src/TodoList.tsx
中 Action
组件的 onClick
方法的参数 key
嘛?它是一个联合类型类型 "complete | delete"
,我们在多出处用到它,现在我们是硬编码写在了程序里,未来这个 key
可能会变化,所以我们需要换成类型别名来表达它们,打开 src/TodoList.tsx
,对其中的内容作出对应的修改如下:
import React from "react";
import { List, Avatar, Menu, Dropdown } from "antd";
import { DownOutlined } from "@ant-design/icons";
import { ClickParam } from "antd/lib/menu";
import { Todo, getUserById } from "./utils/data";
type MenuKey = "complete" | "delete";
interface ActionProps {
onClick: (key: MenuKey) => void;
isCompleted: boolean;
}
// ...
interface TodoListProps {
todoList: Todo[];
onClick: (todoId: string, key: MenuKey) => void;
}
function TodoList({ todoList, onClick }: TodoListProps) {
return (
<List
className="demo-loadmore-list"
itemLayout="horizontal"
dataSource={todoList}
renderItem={item => {
const user = getUserById(item.user);
return (
<List.Item
key={item.id}
actions={[
<Dropdown
overlay={() => (
<Action
isCompleted={item.isCompleted}
onClick={(key: MenuKey) => onClick(item.id, key)}
/>
)}
>
<a key="list-loadmore-more">
操作 <DownOutlined />
</a>
</Dropdown>
]}
>
<List.Item.Meta
avatar={<Avatar src={user.avatar} />}
title={<a href="https://ant.design">{user.name}</a>}
description={item.date}
/>
<div
style={
{
textDecoration: item.isCompleted ? "line-through" : "none"
}}
>
{item.content}
</div>
</List.Item>
);
}}
/>
);
}
export default TodoList;
可以看到,我们定义了一个 MenuKey
类型别名,它表示原联合类型 complete | delete
,然后我们替换了组件中三处使用到这个联合类型的 onClick
函数的参数 key
,将其用 MenuKey
来注解。
其次我们还删除了 antd
和 @ant-design/icons
里面的多余导出。
继续改进
接着我们再来对 TodoList
做一点改变,导出一下我们刚刚定义的 MenuKey
,因为还有其他的地方使用到它,我们打开 src/TodoList.tsx
给 MenuKey
添加 export
前缀,导出我们的类型别名:
// ...
import { Todo, getUserById } from "./utils/data";
export type MenuKey = "complete" | "delete";
interface ActionProps {
onClick: (key: MenuKey) => void;
isCompleted: boolean;
}
// ...
接着我们在 src/App.tsx
里面导入我们的 MenuKey
类型别名,并替换对应的 onClick
的参数 key
的类型注解为 MenuKey
:
import React, { useRef, useState } from "react";
import { Button, Typography, Form, Tabs } from "antd";
import TodoInput from "./TodoInput";
import TodoList from "./TodoList";
import { todoListData } from "./utils/data";
import { MenuKey } from "./TodoList";
import "./App.css";
import logo from "./logo.svg";
// ...
function App() {
const [todoList, setTodoList] = useState(todoListData);
// ...
const activeTodoList = todoList.filter(todo => !todo.isCompleted);
const completedTodoList = todoList.filter(todo => todo.isCompleted);
const onClick = (todoId: string, key: MenuKey) => {
if (key === "complete") {
const newTodoList = todoList.map(todo => {
if (todo.id === todoId) {
return { ...todo, isCompleted: !todo.isCompleted };
}
return todo;
});
setTodoList(newTodoList);
} else if (key === "delete") {
const newTodoList = todoList.filter(todo => todo.id !== todoId);
setTodoList(newTodoList);
}
};
// ...
return (
<div className="App" ref={ref}>
// ...
</div&