# 类型即正义：TypeScript 从入门到实践（二）

## 函数

### 注解函数

``````function add(x, y) {
return x + y;
}

``````function add(x: number, y: number): number {
return x + y;
}

### 函数类型

``````const add = function (x, y) {
return x + y;
}

``````const add: (x: number, y: number): number =  function(x, y) {
return x + y;
}

### 可选参数

``````function buildName(firstName: string, lastName?: string) {
// ...
}

``````buildName('Tom', 'Huang');
buildName('mRcfps');

### 重载

``````let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x): any {
// 如果 x 是 `object` 类型，那么我们返回 pickCard 从 myDeck 里面取出 pickCard1 数据
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// 如果 x 是 `number` 类型，那么直接返回一个可以取数据的 pickCard2
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

let myDeck = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 }
];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

``````let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x): any {
// 如果 x 是 `object` 类型，那么我们返回 pickCard 从 myDeck 里面取出 pickCard1 数据
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// 如果 x 是 `number` 类型，那么直接返回一个可以取数据的 pickCard2
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

let myDeck = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 }
];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

• 第一个重载，我们给参数 `x` 赋值了一个数组类型，数组的项是一个对象，对象包含两个属性 `suit``card` ，它们的类型分别为 `string``number` ；接着返回值类型为 `number` 类型，这个对应 `x` 的类型为 `object` 时，返回类型为 `number` 这种情况。
• 第二个重载，我们给参数 `x` 赋值了一个 `number` 类型，然后返回值类型是一个对象，它有两个属性 `suit``card` ，对应的类型为 `string``number` ；这个对应 `x` 的类型为 `number` 返回值类型为 `object` 类型这种情况。

### 动手实践

``````export interface Todo {
id: string;
user: string;
date: string;
content: string;
isCompleted: boolean;
}

export interface User {
id: string;
name: string;
avatar: string;
}

export function getUserById(userId: string) {
return userList.filter(user => user.id === userId)[0];
}

export const todoListData: Todo[] = [
{
id: "1",
content: "图雀社区：汇聚精彩的免费实战教程",
user: "23410977",
date: "2020年3月2日 19:34",
isCompleted: false
},
{
id: "2",
content: "图雀社区：汇聚精彩的免费实战教程",
user: "23410976",
date: "2020年3月2日 19:34",
isCompleted: false
},
{
id: "3",
content: "图雀社区：汇聚精彩的免费实战教程",
user: "58352313",
date: "2020年3月2日 19:34",
isCompleted: false
},
{
id: "4",
content: "图雀社区：汇聚精彩的免费实战教程",
user: "25455350",
date: "2020年3月2日 19:34",
isCompleted: false
},
{
id: "5",
content: "图雀社区：汇聚精彩的免费实战教程",
user: "12345678",
date: "2020年3月2日 19:34",
isCompleted: true
}
];

export const userList: User[] = [
// ...
{
id: "23410976",
name: "pftom",
avatar: "https://avatars1.githubusercontent.com/u/26423749?s=88&v=4"
},
// ...
{
id: "12345678",
name: "pony",
avatar: "https://avatars3.githubusercontent.com/u/25010151?s=96&v=4"
}
];

• `todoListData` 的每个元素的 `user` 字段改为对应 `userList` 元素的 `id` ，方便基于 `user``id` 进行用户信息的查找。
• 接着我们给 `todoListData` 每个元素添加了 `id` 方便标志，然后把 `time` 属性替换成了 `date` 属性。
• 接着我们定义了一个 `getUserById` 函数，用于每个 `todo` 中根据 `user` 字段来获取对应的用户详情，包括名字和头像等，这里我们有些同学可能有疑问了，我们给参数做了类型注解，为啥不需要注解返回值了？其实这也是 TS 自动类型推断的一个应用场景，TS 编译器会根据参数的类型然后自动计算返回值类型，所以我们就不需要明确的指定返回值啦。
• 最后我们导出了 `Todo``User` 接口。

``````import React from "react";
import { List, Avatar, Menu, Dropdown } from "antd";
import { DownOutlined } from "@ant-design/icons";

import { Todo, getUserById } from "./utils/data";

);

interface TodoListProps {
todoList: Todo[];
}

function TodoList({ todoList }: TodoListProps) {
return (
<List
itemLayout="horizontal"
dataSource={todoList}
renderItem={item => {
const user = getUserById(item.user);

return (
<List.Item
key={item.id}
actions={[
操作 <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;

• 我们首先导入了 `Todo` 接口，给 TodoList 组件增加了 `TodoListProps` 接口用于给这个组件的 `props` 做类型注解。
• 接着我们导入了和 `getUserById` ，用于在 `renderItem` 里面根据 `item.user` 获取用户详情信息，然后展示头像和姓名。
• 接着我们将 `item.time` 更新为 `item.date`
• 最后我们根据待办事项是否已经完成设置了 `line-through``textDecoration` 属性，来标志已经完成的事项。

``````import React, { useRef, useState } from "react";
import {
List,
Avatar,
// ...
Dropdown,
Tabs
} from "antd";

import TodoInput from "./TodoInput";
import TodoList from "./TodoList";

import { todoListData } from "./utils/data";

import "./App.css";
import logo from "./logo.svg";

const { Title } = Typography;
const { TabPane } = Tabs;

function App() {
const [todoList, setTodoList] = useState(todoListData);

const callback = () => {};

const onFinish = (values: any) => {
const newTodo = { ...values.todo, isCompleted: false };
setTodoList(todoList.concat(newTodo));
};
const ref = useRef(null);

const activeTodoList = todoList.filter(todo => !todo.isCompleted);
const completedTodoList = todoList.filter(todo => todo.isCompleted);

return (
<div className="App" ref={ref}>
// ...
<div className="container">
<Tabs onChange={callback} type="card">
<TabPane tab="所有" key="1">
<TodoList todoList={todoList} />
</TabPane>
<TabPane tab="进行中" key="2">
<TodoList todoList={activeTodoList} />
</TabPane>
<TabPane tab="已完成" key="3">
<TodoList todoList={completedTodoList} />
</TabPane>
</Tabs>
</div>
// ...

• 首先我们删除了 `TodoList` 部分代码，转而导入了 `TodoList` 组件
• 接着我们使用 `useState` Hooks 接收 `todoListData` 作为默认数据，然后通过 `isCompleted` 过滤，生成

### 小结

• 首先我们讲解了 TS 中的函数，主要讲解了如何注解函数
• 然后引出了函数赋值给变量时如何进行变量的函数类型注解，并因此讲解了 TS 具有自动类型推断的能力
• 接着，我们对标接口（Interface）讲解了函数也存在可选参数
• 最后我们讲解了 TS 中独有的重载，它主要用来解决函数参数存在多种类型，然后对应参数的不同类型会有不同的返回值类型的情况，那么我们要给这种函数进行类型注解，可以通过重载的方式，解耦参数值类型和返回值类型，将所有可能情况通过重载表现出来。

## 交叉类型、联合类型

### 交叉类型

• 请求成功，返回标志请求成功的状态，以及目标数据
• 请求失败，返回标志请求失败的状态，以及错误信息

``````interface ErrorHandling {
success: boolean;
error?: { message: string };
}

interface ArtistsData {
artists: { name: string }[];
}

const handleArtistsResponse = (response: ArtistsData & ErrorHandling) => {
if (response.error) {
console.error(response.error.message);
return;
}

console.log(response.artists);
};

### 联合类型

``````const value: string = 'Hello Tuture';
const padding: string = '   ';

``````function padLeft(value: string, padding: any) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
}
throw new Error(`Expected string or number, got '\${padding}'.`);
}

``````padLeft('Hello world', true)

``````function padLeft(value: string, padding: string | number) {
// ...中间一样
}

``````padLeft('Hello world', true)

## 字面量类型与类型守卫

### 字面量类型

``````const tutureSlogan: string = 5201314 // 报错 Type '5201314' is not assignable to Type 'string'

• 数字字面量
• 字符串字面量

#### 数字字面量

`520` 这个数，把它当做类型使用，它就是数组字面量类型，使用它来注解一个变量的时候是这样的：

``````let tuture: 520

``````tuture = 520; // 正确
tuture = 521; // 错误 Type '521' is not assignable to type '520'

#### 字符串字面量

``````let tuture: '520';

tuture = '520';
tuture = '521'; // Type '"521"' is not assignable to type '"520"'

• 实现枚举
• 实现类型守卫

#### 搭配举例 - 实现枚举效果

``````function getUserInfo(osType: 'Linux' | 'Mac' | 'Windows') { // ... 后续实现 }

``````enum EnumOSType {
Linux,
Mac,
Windows
}

function getUserInfo(osType: EnumOSType) {}

### 类型守卫

``````interface Linux {
type: 'Linux';
linuxUserInfo: '极客';
}

interface Mac {
type: 'Mac';
macUserInfo: '极客+1';
}

interface Windows {
type: 'Windows';
windowsUserInfo: '极客+2';
}

function getUserInfo(os: Linux | Mac | Windows) {
console.log(os.linuxUserInfo);
}

``````function getUserInfo(os: Linux | Mac | Windows) {
switch (os.type) {
case 'Linux': {
console.log(os.linuxUserInfo);
break;
}

case 'Mac': {
console.log(os.macUserInfo);
break;
}

case 'Windows': {
console.log(os.windowsUserInfo);
break;
}
}
}

### 动手实践

``````import React from "react";
import { List, Avatar, Menu, Dropdown, Modal } from "antd";
import { DownOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
import { ClickParam } from "antd/lib/menu";

import { Todo, getUserById } from "./utils/data";

const { confirm } = Modal;

interface ActionProps {
onClick: (key: "complete" | "delete") => void;
isCompleted: boolean;
}

function Action({ onClick, isCompleted }: ActionProps) {
const handleActionClick = ({ key }: ClickParam) => {
if (key === "complete") {
onClick("complete");
} else if (key === "delete") {
onClick("delete");
}
};

return (
);
}

interface TodoListProps {
todoList: Todo[];
onClick: (todoId: string, key: "complete" | "delete") => void;
}

function TodoList({ todoList, onClick }: TodoListProps) {
return (
<List
// ...
<List.Item
key={item.id}
actions={[
<Dropdown
overlay={() => (
<Action
isCompleted={item.isCompleted}
onClick={(key: "complete" | "delete") =>
onClick(item.id, key)
}
/>
)}
>
操作 <DownOutlined />
</a>
// ...

• 我们扩展了单个 Todo 的点击下拉菜单的菜单组件，定义了一个 `Action` 组件，它接收两个参数，`isCompleted``onClick` ，前者用来标志现在对 Todo 操作是重做还是完成，后者用来处理点击事件，根据 `todo.id` 和 操作的类型 `key` 来处理。
• 我们在 `Action` 组件的 `onClick` 属性里面调用的 `onClick` 函数是父组件传下来的函数，所以我们需要额外在 `TodoListProps` 加上这个 `onClick` 函数的类型定义，按照我们之前学习的注解函数的知识，这里我们需要注解参数列表和返回值，因为 `onClick` 函数内部执行点击逻辑，不需要返回值，所以我们给它注解了 `void` 类型，针对参数列表，`todoId` 比较简单，一般是字符串，所以注解为 `string` 类型，而 `key` 标注操作的类型，它是一个字面量联合类型，允许有 `complete``delete` 两种
• 接着我们来看 Action 组件，我们在上一步已经讲解它接收两个参数，因此我们新增一个 `ActionProps` 来注解 Action 组件的参数列表，可以看到其中的 `onClick` 和我们上一步讲解的一样，`isCompleted` 注解为 `boolean`
• 接在在 Action 组件里我们定义了 Menu `onClick`的处理函数 `handleActionClick` 是一个`ClickParam` 类型，它是从 `antd/lib/menu` 导入的 ，由组件库提供的，然后我们从参数里面解构出来了 `key` ，进而通过字面量类型进行类型守卫，处理了对于的 `onClick` 逻辑
• 最后我们做的一点改进就是在 Menu 里面根据 `isCompleted` 展示 “重做” 还是 “完成”。

``````import React, { useRef, useState } from "react";
import {
List,
Avatar,
// ...
function App() {
const [todoList, setTodoList] = useState(todoListData);

const callback = () => {};
// ...
const activeTodoList = todoList.filter(todo => !todo.isCompleted);
const completedTodoList = todoList.filter(todo => todo.isCompleted);

const onClick = (todoId: string, key: "complete" | "delete") => {
if (key === "complete") {
const newTodoList = todoList.map(todo => {
if (todo.id === todoId) {
return { ...todo, isCompleted: !todo.isCompleted };
}

});

setTodoList(newTodoList);
} else if (key === "delete") {
const newTodoList = todoList.filter(todo => todo.id !== todoId);
setTodoList(newTodoList);
}
};

return (
<div className="App" ref={ref}>
// ...
<div className="container">
<Tabs onChange={callback} type="card">
<TabPane tab="所有" key="1">
<TodoList todoList={todoList} onClick={onClick} />
</TabPane>
<TabPane tab="进行中" key="2">
<TodoList todoList={activeTodoList} onClick={onClick} />
</TabPane>
<TabPane tab="已完成" key="3">
<TodoList todoList={completedTodoList} onClick={onClick} />
</TabPane>
</Tabs>
</div>
</div>
);
}

export default App;

• `TodoList` 增加 `onClick` 属性
• 实现 `onClick` 函数，根据字面量类型 `key` 进行类型守卫处理对应的数据更改逻辑