现在我们不再使用ts-node, 改用官方的typescript编译器,官方编译器从 .ts文件生成和打包 JavaScript 文件
setting up the project
在一个空目录**npm init**初始化,安装typescript
npm install typescript --save-dev
TypeScript的原生tsc 编译器可以帮助我们使用命令**tsc --init** 初始化我们的项目,生成我们的tsconfig.json 文件。
(全局安装了typescript才可以直接使用tsc —init, 即使全局安装也应该安装成开发依赖)
在可执行脚本列表scripts中添加tsc命令
{// .."scripts": {"tsc": "tsc",},// ..}
运行如下命令初始化tsconfig.json
npm run tsc -- --init
我们需要的配置
{"compilerOptions": {"target": "ES6","outDir": "./build/","module": "commonjs","strict": true,"noUnusedLocals": true,"noUnusedParameters": true,"noImplicitReturns": true,"noFallthroughCasesInSwitch": true,"esModuleInterop": true}}
安装express和eslint
npm install expressnpm install --save-dev eslint @types/express @typescript-eslint/eslint-plugin @typescript-eslint/parser
创建.eslintrc文件, 配置如下
{"extends": ["eslint:recommended","plugin:@typescript-eslint/recommended","plugin:@typescript-eslint/recommended-requiring-type-checking"],"plugins": ["@typescript-eslint"],"env": {"browser": true,"es6": true,"node": true},"rules": {"@typescript-eslint/semi": ["error"],"@typescript-eslint/explicit-function-return-type": "off","@typescript-eslint/explicit-module-boundary-types": "off","@typescript-eslint/restrict-template-expressions": "off","@typescript-eslint/restrict-plus-operands": "off","@typescript-eslint/no-unsafe-member-access": "off","@typescript-eslint/no-unused-vars": ["error",{ "argsIgnorePattern": "^_" }],"no-case-declarations": "off"},"parser": "@typescript-eslint/parser","parserOptions": {"project": "./tsconfig.json"}}
安装ts-node-dev自动重载
npm install --save-dev ts-node-dev
在scripts中定义几个脚本
{// ..."scripts": {"tsc": "tsc","dev": "ts-node-dev index.ts","lint": "eslint --ext .ts ."},// ...}
Let there be code
index.ts
import express from 'express'const app = express()app.use(express.json())const PORT = 3000app.get('/ping', (_req, res) => {console.log('someone pinged here')res.send('pong')})app.listen(PORT, () => {console.log(`Server running on port ${PORT}`)})
运行命令 **npm run tsc** 将代码转译为js, 生成build目录
新建.eslintignore文件,让ESLint忽略build目录
在scripts中添加start命令
{"scripts": {// ..."start": "node build/index.js"},}
运行命令 **npm run start**, 应用则在生产模式下运行
exercise 9.8 - 9.9
解决跨域问题
在后端项目中安装cors
npm install cors
在后端代码中使用cors
import express from 'express'import cors from 'cors'const app = express()app.use(express.json())// eslint-disable-next-line @typescript-eslint/no-unsafe-callapp.use(cors())
eslint报错,需安装@types/cors
npm i --save-dev @types/cors
Implementing the functionality
项目目录结构
这次采用硬编码的数据,json文件放在data目录
[{"id": 1,"date": "2017-01-01","weather": "rainy","visibility": "poor","comment": "Pretty scary flight, I'm glad I'm alive"},{"id": 2,"date": "2017-04-01","weather": "sunny","visibility": "good","comment": "Everything went better than expected, I'm learning much"},// ...]

如果导入的是json文件,编辑器会报错,需要在tsconfig.json中添加如下配置
{"compilerOptions": {// ..."resolveJsonModule": true}}

文件json有自己的值,如果赋值给指定了类型的变量时,会报错
可以使用类型断言(即as语法),给json数据指定类型
const diaries: Array<DiaryEntry> = diaryData as Array<DiaryEntry>;
node会按如下顺序对文件进行解析
["js", "json", "node", "ts", "tsx"]
如果配置了**"resolveJsonModule": true**,
当有如下文档结构时
├── myModule.json└── myModule.ts
想通过下列方式引入ts文件不会成功,因为json的优先级是高于ts的
import myModule from "./myModule";
所以应避免文件名重复
因为json不支持类型,改用ts文件
import { DiaryEntry } from '../src/types'const DiaryEntries: Array<DiaryEntry> = [{id: 1,date: '2017-01-01',weather: 'rainy',visibility: 'poor',comment: "Pretty scary flight, I'm glad I'm alive",},// ...]export default DiaryEntries
将路由从index.ts中分离到src/routes目录中,
import express from 'express'import diaryService from '../services/disaryService'const router = express.Router()router.get('/', (_req, res) => {res.send(diaryService.getNonSensitiveEntries())})router.post('/', (_req, res) => {res.send('Saving a diary!')})export default router
将业务逻辑分离到src/services目录中
import diaries from '../../data/diaries'import { NonSensitiveDiaryEntry, DiaryEntry } from '../types'const getEntries = (): Array<DiaryEntry> => {return diaries}const getNonSensitiveEntries = (): NonSensitiveDiaryEntry[] => {return diaries.map(({ id, date, weather, visibility }) => ({id,date,weather,visibility,}))}const addDiary = () => {return []}export default {getEntries,addDiary,getNonSensitiveEntries,}
在index.ts中使用路由
import express from 'express'import diaryRouter from './routes/diaries'const app = express()app.use(express.json())const PORT = 3000app.get('/ping', (_req, res) => {console.log('someone pinged here')res.send('pong')})app.use('/api/diaries', diaryRouter)app.listen(PORT, () => {console.log(`Server running on port ${PORT}`)})
类型声明文件为根目录的types.ts文件
export type Weather = 'sunny' | 'rainy' | 'cloudy' | 'windy' | 'stormy'export type Visibility = 'great' | 'good' | 'ok' | 'poor'export interface DiaryEntry {id: numberdate: stringweather: Weathervisibility: Visibilitycomment?: string}export type NonSensitiveDiaryEntry = Omit<DiaryEntry, 'comment'>
| 分隔的为联合类型
用 ?定义可选属性, 如 comment?: string
如果从联合类型中选取一部分构成新的类型,可以使用Pick工具类型
const getNonSensitiveEntries =(): Array<Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>> => {// ...}
使用嵌套的语法可能有点奇怪,可以使用alternative数组类型
const getNonSensitiveEntries =(): Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>[] => {// ...}
如果只想排除某一个字段,可以使用Omit工具类型
const getNonSensitiveEntries = (): Omit<DiaryEntry, 'comment'>[] => {// ...}
Preventing an accidental undefined result
将后端扩展为路由 api/diaries/:id来支持获取一个特定条目
service
const findById = (id: number): DiaryEntry | undefined => {const entry = diaries.find((d) => d.id === id)return entry}export default {getEntries,addDiary,getNonSensitiveEntries,findById,}
有可能获取不到记录,所以要加undefined
router
import express from 'express';import diaryService from '../services/diaryService'router.get('/:id', (req, res) => {const diary = diaryService.findById(Number(req.params.id));if (diary) {res.send(diary);} else {res.sendStatus(404);}})// ...export default router;
Adding a new diary
通过post添加新条目
在types中声明NewDiaryEntry类型
export type NewDiaryEntry = Omit<DiaryEntry, 'id'>
router
/* eslint-disable @typescript-eslint/no-unsafe-assignment */// ...router.post('/', (req, res) => {const { date, weather, visibility, comment } = req.bodyconst newDiaryEntry = diaryService.addDiary({date,weather,visibility,comment,})res.json(newDiaryEntry)})
service
const addDiary = (entry: NewDiaryEntry): DiaryEntry => {const newDiaryEntry = {id: Math.max(...diaries.map((d) => d.id)) + 1,...entry,}diaries.push(newDiaryEntry)return newDiaryEntry}
为了解析传入的数据,我们必须配置json 中间件:
import express from 'express';import diaryRouter from './routes/diaries';const app = express();app.use(express.json());const PORT = 3000;app.use('/api/diaries', diaryRouter);app.listen(PORT, () => {console.log(`Server running on port ${PORT}`);});
使用vscode插件Rest Client测试, 在request目录新建一个.rest后缀的文件
使用postman测试
Proofing requests
校对请求
来自外部的数据不能完全信任,需要进行类型判断
在src目录新建utils.ts文件,外部数据设为unknown类型
import { NewDiaryEntry } from './types';const toNewDiaryEntry = (object: unknown): NewDiaryEntry => {const newEntry: NewDiaryEntry = {// ...}return newEntry;}export default toNewDiaryEntry;
在route中使用这个函数
import toNewDiaryEntry from '../utils';// ...router.post('/', (req, res) => {try {const newDiaryEntry = toNewDiaryEntry(req.body);const addedEntry = diaryService.addDiary(newDiaryEntry);res.json(addedEntry);} catch (e) {res.status(400).send(e.message);}})
需要对unknown类型的数据进行逐一校验
为每个字段创造解析器
comment字段
const parseComment = (comment: unknown): string => {if (!comment || !isString(comment)) {throw new Error('Incorrect or missing comment')}return comment}
isString函数是类型守卫(type guard), 它的返回类型text is string是类型谓词(type predicate), 格式是_**parameterName is Type**_,如果函数返回true, 则可以推断参数是类型谓词中的类型
const isString = (text: unknown): text is string => {return typeof text === 'string' || text instanceof String}
可以看到,传入参数时comment类型为unknown, 返回时ts推断它的类型为string
为什么string的判断要用2种?typeof text === 'string' || text instanceof String
因为可以通过2种方式声明string
const a = "I'm a string primitive";const b = new String("I'm a String Object");typeof a; --> returns 'string'typeof b; --> returns 'object'a instanceof String; --> returns falseb instanceof String; --> returns true
校验日期
const isDate = (date: string): boolean => {return Boolean(Date.parse(date))}const parseDate = (date: unknown): string => {if (!date || !isString(date) || !isDate(date)) {throw new Error('Incorrect or missing date: ' + date)}return date}
校验weather
const parseWeather = (weather: unknown): Weather => {if (!weather || !isString(weather) || !isWeather(weather)) {throw new Error('Incorrect or missing weather: ' + weather)}return weather}const isWeather = (str: string): str is Weather => {return ['sunny', 'rainy', 'cloudy', 'stormy'].includes(str)}
在isWeather函数中,如果Weather类型的定义改变了,而这里没有一起更改,则会出现问题
此时,应该使用枚举类型(TypeScript enum)
枚举类型允许在程序运行时获取它的值
将Weather修改为枚举类型
export enum Weather {Sunny = 'sunny',Rainy = 'rainy',Cloudy = 'cloudy',Stormy = 'stormy',Windy = 'windy',}
修改isWeather函数
// eslint-disable-next-line @typescript-eslint/no-explicit-anyconst isWeather = (param: any): param is Weather => {return Object.values(Weather).includes(param);};
Object.values返回一个数组
这里param要用any类型,string不是枚举类型
当有一组预先确定的数值预期在将来不会发生变化时,通常使用枚举。
此时data中会报错, 因为字符串不是枚举类型
使用toNewDiaryEntry函数修复这个问题
import { DiaryEntry } from "../src/types";import toNewDiaryEntry from "../src/utils";const data = [{"id": 1,"date": "2017-01-01","weather": "rainy","visibility": "poor","comment": "Pretty scary flight, I'm glad I'm alive"},// ...]const diaryEntries: DiaryEntry [] = data.map(obj => {const object = toNewDiaryEntry(obj) as DiaryEntry;object.id = obj.id;return object;});export default diaryEntries;
最后再来重新定义toNewDiaryEntry函数
const toNewDiaryEntry = (object: unknown): NewDiaryEntry => {const newEntry: NewDiaryEntry = {comment: parseComment(object.comment),date: parseDate(object.date),weather: parseWeather(object.weather),visibility: parseVisibility(object.visibility)};return newEntry;};
unknown类型是不允许任何操作的,无法访问object的属性,所以上面的代码无效
可以通过解构变量修复这个问题
type Fields = { comment : unknown, date: unknown, weather: unknown, visibility: unknown };const toNewDiaryEntry = ({ comment, date, weather, visibility } : Fields): NewDiaryEntry => {const newEntry: NewDiaryEntry = {comment: parseComment(comment),date: parseDate(date),weather: parseWeather(weather),visibility: parseVisibility(visibility)};return newEntry;};
或者使用any类型
// eslint-disable-next-line @typescript-eslint/no-explicit-anyconst toNewDiaryEntry = (object: any): NewDiaryEntry => {const newEntry: NewDiaryEntry = {comment: parseComment(object.comment),date: parseDate(object.date),weather: parseWeather(object.weather),visibility: parseVisibility(object.visibility)};return newEntry;};
exercise 9.12 - 9.13
使用uuid
安装
npm install uuidnpm i --save-dev @types/uuid
使用
import {v1 as uuid} from 'uuid'const id = uuid()
