2. TypeScript概述

2.1 编译器

程序由编译器解析,转换成抽象语法树(abstract syntax tree),AST去掉了空白、注释和缩进用的制表符或空格之后的数据结构。编译器把AST转换成一种字节码的底层表示。字节码再传给运行时程序计算,得到最终结果。不同的语言的具体细节有所不同,但是整体大致如此。

步骤如下:

  1. 把程序解析为AST
  2. 把AST编译成字节码
  3. 运行时计算字节码

:::tips TypeScript编译器生成AST之后,真正运行代码之前,TypeScript会对代码做类型检查,这正是它的魔力所在
类型检查器:检查代码是否符合类型安全要求的特殊程序 :::

TypeScript编译过程 (TSC操作

  1. TypeScript源码 -> TypeScript AST
  2. 类型检查器检查AST
  3. TypeScript AST -> JavaScript源码

JavaScript编译过程 (浏览器、NodeJS或其他JavaScript引擎中的JavaScript运行时操作)

  1. JavaScript源码 -> JavaScript AST
  2. AST -> 字节码
  3. 运行时计算字节码

:::info JavaScript编译器和运行时通常聚在一个称为引擎的过程中。程序员一般就是与引擎交互的。
V8(驱动NodeJS、Chorme和Opera的引擎)、SpiderMonkey(Firefox)、JSCore(Safari)和Chakra(Edge)都是如此。
所以JavaScrip看起来像是一门解释型语言 :::

2.2 类型系统

即类型检查器为程序分配类型时使用的一系列规则
一般语言有两种

  1. 显示值类型
  2. 自动推导值类型

TypeScript身兼两种类型系统

  1. 显式注解类型 -> let a: number = 1
  2. 自动推导类型 -> let a = 1 :::tips 一般来说,最好让TypeScript自动推导类型,少数情况才显示注解类型 :::

比较TypeScript和JavaScript的类型系统

类型系统特性 TypeScript JavaScript
类型是如何绑定的? 静态 动态
是否自动转换类型? 否(多数时候)
何时检查类型? 编译时 运行时
何时报告错误 编译时(多数时候) 运行时(多数时候)

类型是如何绑定的?
JavaScript动态绑定类型,因此必须运行程序才能知道类型。再运行之前,对类型一无所知
TypeScript是渐进式类型语言。这意味着,在编译时知道所有类型能让TypeScript充分发挥作用,但在编译程序之前,并不需要知道全部类型。即便没有类型的程序,TypeScript也能推导出部分类型,捕获部分错误,但这并不全面,大量错误可能会暴露给用户

:::tips 尽管如此,还有大量错误是TypeScript在编译时无法捕获的,例如堆栈溢出、网络断连和恶意的用户输入,这些属于运行时异常。TypeScript所能做的是把纯JavaScript代码中那些运行时错误提前到编译时报告 :::

3. 类型全解

Screen Shot 2021-02-21 at 16.40.59.png
TypeScript的类型层次结构

3.2 类型浅谈

3.2.3 boolean

  1. let a = true // boolean ts推导值的类型为boolean
  2. var b = false // boolean ts推导值的类型为boolean
  3. let b: boolean = true // boolean 显式注解,告诉ts,值的类型boolean
  4. const c = true // true ts推导出字面量类型,赋值后无法修改
  5. let e: true = true // true 显式注解,告诉ts,限定了e只能在所有布尔值中只能取true

类型字面量:仅表示一个值的类型(上述的4和5行)

3.2.8 对象

  1. // 显式注解
  2. let a: {
  3. b: number // 必须定义
  4. c?: string // 可选
  5. [key: number]: boolean // 索引签名
  6. } = {
  7. b: 1
  8. }
  9. // 不显式注解
  10. let a = {
  11. b: 'x'
  12. }
  13. // 上述两种均为对象字面量句法

TypeScript中声明对象类型有四种方式

  1. 对象字面量表示法({a: string})
  2. 空对象字面量表示法({})。不推荐
  3. object类型。如果需要一个对象,但对对象的字段没有要求,使用这种方式
  4. Object类型。不推荐

3.2.10 数组

  1. let a = [1, 2, 3] // number[]
  2. var b = ['a', 'b'] // string[]
  3. let c: string[] = ['a'] // string[] 显式注解
  4. let d = [1, 'a'] // (string | number)[]
  5. const e = [2, 'b'] // (string | number)[]
  6. let g = [] // any[]

3.2.11 元祖

元祖(Tuple)是array的子类型,允许表示一个已知元素数量和类型的数组,各元素的类型不必相同,声明元祖必须显式注解

  1. let a: [number, string] = [1, 'a']
  2. let list: [string, ...string[]] = ['sara', 'tali'] //至少有一个字符串元素
  3. let list1: [number, boolean, ...string[]] = [1, true, 'b'] // 前面两个元素必须定义

只读数组和元组

  1. // 数组
  2. type A = readonly string[] // readonly string[]
  3. type B = ReadonlyArray<string> // readonly string[]
  4. type C = Readonly<string[]> // readonly string[]
  5. // 元祖
  6. type D = readonly [number, string] // readonly [number, string]
  7. type E = Readonly<[number, string]> // readonly [number, string]

4. 函数

4.1.1 可选和默认参数

  1. function log(message: string, userId = 'id') { // 默认参数 普通类型让TS自己推导
  2. console.log(message, userId)
  3. }
  4. type Context = {
  5. appId?: string
  6. userId?: string
  7. }
  8. function log1(message: string, context: Context = {}) { // 默认参数 对象等引用类型推荐注解
  9. console.log(message, context.appId)
  10. }

:::tips

  1. 非默认参数大部分情况显式注解比较好,因为函数体内往往会进行运算等一系列操作,TS多数情况下无法推导出参数的类型
  2. 参数显式注解了,函数的返回值可以不显式注解,让TS自动推导,避免重复劳动(当然如果你明确知道函数的返回类型,也可以注解吧~) :::

4.1.4 注解this的类型

  1. function fancyDate(this: Date) {
  2. return this.getDate()
  3. }

:::info

  1. 如果函数使用this,请在函数的第一个参数中声明this类型。this不是常规参数,而是保留字,是函数签名的一部分

注意:开启strict会自动启用noImplicitThis(强制显示注解函数中this的类型,不强制类或对象的函数注解this) :::

4.1.7 调用签名

函数的调用签名只包含类型层面的代码,即只有类型,没有值。因此,函数的调用签名可以表示参数的类型、this的类型、返回值的类型、剩余参数的类型和可选参数的类型,但是无法表示默认值(因为默认值是值,不是类型)。调用签名没有函数的定义体,无法推导出返回类型,所以必须显式注解

  1. // 简写调用签名
  2. type Log = (message: string, user?: string) => void
  3. // 完整调用签名
  4. type Log = {
  5. (message: string, user?: string): void
  6. }
  7. let log: Log = (message, user = 'testUser') => {....}
  8. // 上述两种写法完全等效,首选简写形式

:::info 使用调用签名的好处就是可以明确体现函数的具体类型,这一点是普通函数做不到的 :::

4.1.9 重载

简单讲就是函数的输出类型取决于参数的输入类型

  1. type CreateElement = {
  2. (tag: 'canvas'): HTMLCanvasElement
  3. (tag: 'table'): HTMLTableElement
  4. (tag: string): HTMLElement // 兜底,类似any
  5. }
  6. // 函数表达式
  7. let createElement: CreateElement = (tag: string): HTMLElement => {
  8. ....
  9. }
  10. // 函数声明
  11. function createElement(tag: 'canvas'): HTMLCanvasElement
  12. function createElement(tag: 'table'): HTMLTableElement
  13. function createElement(tag: string): HTMLElement {
  14. if(tag === 'canvas') {
  15. return document.createElement(tag)
  16. } else if(tag === 'table') {
  17. return document.createElement(tag)
  18. } else {
  19. // ...
  20. }
  21. }

5. 类和接口

5.2 类

与传统面向对象编程语言差不多,就不细讲了

5.4 接口

类型别名相似,接口是一种命名类型的方式,这样就不用在行内定义了(即定义与实现分离)。
两者算是同一概念的句法,下面列出共同点与不同点

共同点

  1. // 类型别名
  2. type Sushi = {
  3. calories: number
  4. salty: boolean
  5. }
  6. // 接口
  7. interface Sushi = {
  8. calories: number
  9. salty: boolean
  10. }
  1. // 类型别名
  2. type Food = {
  3. calories: number
  4. tasty: boolean
  5. }
  6. type Sushi = Food & { // 交集,3个属性都需定义实现
  7. salty: boolean
  8. }
  9. type Cake = Food & {
  10. sweet: boolean
  11. }
  12. // 接口
  13. interface Food = {
  14. calories: number
  15. tasty: boolean
  16. }
  17. interface Sushi extends Food { // 继承,3个属性都需定义实现
  18. salty: boolean
  19. }
  20. interface Cake extends Food {
  21. sweet: boolean
  22. }

不同点

  1. 类型别名更为通用,右边可以是任何类型;而在接口声明中,右边必须为结构 ```typescript type A = number type B = A | string

interface … = {

}

  1. 2. 扩展接口时,TypeScript将检查扩展的接口是否可赋值给被扩展的接口
  2. ```typescript
  3. interface A {
  4. good(x: number): string
  5. bad(x: number): string
  6. }
  7. interface B extends A {
  8. good(x: string | number): string
  9. bad(x: string): string // Error TS....
  10. }
  11. // 接口换成类型别名,extends换成交集&不会出现这种问题
  1. 同一作用域中的多个同名接口将自动合并(声明合并);而多个同名类型别名将导致编译错误。

:::info 总结
type -> inteface -> class (复杂度及丰富等级)

  1. 简单结构推荐使用type
  2. 中等复杂结构推荐使用interface(接口使用的最多)
  3. 跟业务相关类或结构扩展性更强的推荐使用class :::

6. 类型进阶

6.6 解决办法

6.6.1 类型断言

明确告诉TS我知道类型

  1. // 尖括号语法 (JSX语法不支持)
  2. let someValue: any = "this is a string";
  3. let strLength: number = (<string>someValue).length;
  4. // 推荐使用as语法
  5. let someValue: any = "this is a string";
  6. let strLength: number = (someValue as string).length;

6.6.2 非空断言

如果编译器不能够去除 nullundefined,你可以使用非空断言手动去除。 语法是添加 !后缀: identifier!identifier的类型里去除了 nullundefined

6.6.3 明确赋值断言

  1. let userId: string
  2. fetchUser()
  3. userId.toUpperCase() // Error....
  4. function fetchUser() {
  5. userId = globalCache.get('userId')
  6. }
  7. // 改进
  8. let userId!: string // 加了!,明确告诉ts在读取userId时,肯定已经赋值了
  9. fetchUser()
  10. userId.toUpperCase()
  11. function fetchUser() {
  12. userId = globalCache.get('userId')
  13. }

:::tips 总结:断言语句应尽可能少使用,如果经常使用,可能表明你的代码有问题 :::

7. 异常

  1. // error.ts
  2. export class InvalidDateFormatError extends RangeError {}
  3. export class DateIsInTheFutureError extends RangeError {}
  1. import { InvalidDateFormatError, DateIsInTheFutureError } from './error'
  2. function isVaild(data: Date) {
  3. return Object.prototype.toString.call(date) === '[object Date]'
  4. && !Number.isNaN(date.getTime())
  5. }
  6. function parse(birthday: string): Date {
  7. let date = new Date(birthday)
  8. if(!isVaild(date)) {
  9. throw new InvalidDateFormatError('Enter a date in the form YYYY/MM/DD')
  10. }
  11. if(date.getTime() > Date.now()) {
  12. throw new DateIsInTheFutureError('Are you a timelord?')
  13. }
  14. return date
  15. }
  16. try{
  17. let date = parse(ask())
  18. } catch(e) {
  19. if(e instanceof InvalidDateFormatError) {
  20. console.error(e.message)
  21. } else if (e instanceof DateIsInTheFutureError) {
  22. console.info(e.message)
  23. } else {
  24. throw e
  25. }
  26. }

9. 前后端框架

9.1 前端框架

  1. // tsconfig.json
  2. {
  3. "compilerOptions": {
  4. "lib": ["dom"], // 项目中使用DOM API
  5. "allowSyntheticDefaultImports": true, // 可使用默认导入语法 import React from "react"
  6. "jsx": "react" // 开启TSX(JSX + TypeScript)
  7. }
  8. }

9.1.1 React

函数式组件

  1. import React from 'react'
  2. type Props = { // 也可以用interface
  3. isDisabled?: boolean
  4. size: 'Big' | 'Small'
  5. onClick(event: React.MouseEvent<HTMLButtonElement>): void
  6. }
  7. export function FancyButton(props: Props) {
  8. const [toggled, setToggled] = React.useState(false) // Hook
  9. return <button
  10. className = {props.size}
  11. disabled = {props.isDisabled || false}
  12. onClick = {event => {
  13. setToggled(!toggled)
  14. props.onClick(event)
  15. }}
  16. >{props.size}</button>
  17. }
  18. let button = <FancyButton
  19. size='Big'
  20. onClick = {() => {console.log('clicked')}}
  21. />

类组件

  1. import React from 'react'
  2. import { FancyButton } from './FancyButton'
  3. type Props = {
  4. firstName: string
  5. userId: string
  6. }
  7. type State = {
  8. isLoading: boolean
  9. }
  10. class SignupForm extends React.Component<Props, State> {
  11. state = {
  12. isLoading: false
  13. }
  14. private signUp = async() => {
  15. this.setState({ isLoading: true })
  16. try{
  17. await fetch('/api/sign...' + this.props.userId)
  18. } finally {
  19. this.setState({ isLoading: false })
  20. }
  21. }
  22. render() {
  23. return (
  24. <>
  25. <FancyButton
  26. isDisabled={this.state.isLoading}
  27. size='Big'
  28. onClick={this.signUp}
  29. />
  30. </>
  31. )
  32. }
  33. }
  34. let form = <SignupForm firstName='name' userId='13ab' />

:::tips

  1. 事件类型参考event
  2. 更多用法可参考typescript-cheatsheets :::

11. 与JavaScript互操作

11.3 寻找JavaScript代码的类型信息

  1. 在TypeScript文件中导入JavaScript文件
    1. 在同一级目录中寻找与.js文件同名的.d.ts文件
    2. 如果不存在这样的文件,而且allowJscheckJs的值为true,推导.js文件的类型信息
    3. 如果无法推导,把整个模块视作any
  2. 在TypeScript文件中导入npm包
    1. 在本地寻找模块的类型声明,找到就用 (type.d.ts)
    2. 本地找不到,再分析模块package.json,如果文件中定义了名为types或typings的字段,使用字段设置的.d.ts文件做为模块的类型声明源
    3. 如果没有上述字段,沿着目录结构逐级向上,寻找node_modules/@types文件夹

image.png
(import React from ‘react’ 会找到@types/react/index.d.ts)

  1. // tsconfig.json
  2. {
  3. "compilerOptions": {
  4. "typeRoots": ["./typings", "./node_modules/@types"], //修改寻找全局类型声明的默认行为 (默认是type.d.ts)
  5. "types": ["react"] // 忽略所有第三方类型声明从types中找,react除外
  6. }
  7. }

11.4 使用第三方JavaScript

使用NPM在项目中安装的第三方库大致分为三种情况

  1. 安装的代码自带类型声明
  2. 安装的代码没有自带类型声明,不过DefinitelyTyped中有相应的类型声明
  3. 安装的代码没有自带类型声明,而且DefinitelyTyped中也没有相应的类型声明

11.4.1 自带类型声明的JavaScript

11.4.2 DefinitelyTyped中有类型声明的JavaScript

DefinitelyTyped(类似一个社区),为众多开源项目提供外参模块声明,DefinitelyTyped中的所有类型声明都已发布到NPM中,放在@types作用域下,可以放心使用,检查你的安装的包在DefinitelyTyped中有没有类型声明

11.4.3 DefinitelyTyped中没有类型声明的JavaScript

  1. 在导入的文件上加上 @ts-ignore 指令,全部内容是any类型

    1. // @ts-ignore
    2. import module from 'untyped-module'
  2. 创建一个空白的类型声明文件,新建一个类型声明文件(type.d.ts),写入下述外参类型声明,全是any类型

    1. // type.d.ts
    2. declare module 'nearby-ferret-alerter'
  3. 自己编写外参模块声明…

  4. 自己编写类型声明,发布到NPM中

总结

方式 tsconfig.json 类型安全性
导入无类型的js {“allowJS”: true} 较差
导入并检查js {“allowJS”: true, “checkJs”: true} 尚可
导入并检查有JSDoc注解的js {“allowJS”: true, “checkJs”: true, “strict”: true} 极好
导入带类型声明的js {“allowJS”: false, “strict”: true} 极好
导入TS {“allowJS”: false, “strict”: true} 极好