- 2. TypeScript概述
- 3. 类型全解
- 4. 函数
- 5. 类和接口
- 6. 类型进阶
- 7. 异常
- 9. 前后端框架
- 11. 与JavaScript互操作
- 11.3 寻找JavaScript代码的类型信息
- 11.4 使用第三方JavaScript
- 11.4.1 自带类型声明的JavaScript
- DefinitelyTyped中有类型声明的JavaScript">11.4.2 DefinitelyTyped中有类型声明的JavaScript
- DefinitelyTyped中没有类型声明的JavaScript">11.4.3 DefinitelyTyped中没有类型声明的JavaScript
2. TypeScript概述
2.1 编译器
程序由编译器解析,转换成抽象语法树(abstract syntax tree),AST去掉了空白、注释和缩进用的制表符或空格之后的数据结构。编译器把AST转换成一种字节码的底层表示。字节码再传给运行时程序计算,得到最终结果。不同的语言的具体细节有所不同,但是整体大致如此。
步骤如下:
- 把程序解析为AST
- 把AST编译成字节码
- 运行时计算字节码
:::tips
TypeScript编译器生成AST之后,真正运行代码之前,TypeScript会对代码做类型检查,这正是它的魔力所在
类型检查器:检查代码是否符合类型安全要求的特殊程序
:::
TypeScript编译过程 (TSC操作)
- TypeScript源码 -> TypeScript AST
- 类型检查器检查AST
- TypeScript AST -> JavaScript源码
JavaScript编译过程 (浏览器、NodeJS或其他JavaScript引擎中的JavaScript运行时操作)
- JavaScript源码 -> JavaScript AST
- AST -> 字节码
- 运行时计算字节码
:::info
JavaScript编译器和运行时通常聚在一个称为引擎的过程中。程序员一般就是与引擎交互的。
V8(驱动NodeJS、Chorme和Opera的引擎)、SpiderMonkey(Firefox)、JSCore(Safari)和Chakra(Edge)都是如此。
所以JavaScrip看起来像是一门解释型语言
:::
2.2 类型系统
即类型检查器为程序分配类型时使用的一系列规则
一般语言有两种
- 显示值类型
- 自动推导值类型
TypeScript身兼两种类型系统
- 显式注解类型 -> let a: number = 1
- 自动推导类型 -> let a = 1 :::tips 一般来说,最好让TypeScript自动推导类型,少数情况才显示注解类型 :::
比较TypeScript和JavaScript的类型系统
| 类型系统特性 | TypeScript | JavaScript |
|---|---|---|
| 类型是如何绑定的? | 静态 | 动态 |
| 是否自动转换类型? | 否(多数时候) | 是 |
| 何时检查类型? | 编译时 | 运行时 |
| 何时报告错误 | 编译时(多数时候) | 运行时(多数时候) |
类型是如何绑定的?
JavaScript动态绑定类型,因此必须运行程序才能知道类型。再运行之前,对类型一无所知
TypeScript是渐进式类型语言。这意味着,在编译时知道所有类型能让TypeScript充分发挥作用,但在编译程序之前,并不需要知道全部类型。即便没有类型的程序,TypeScript也能推导出部分类型,捕获部分错误,但这并不全面,大量错误可能会暴露给用户
:::tips 尽管如此,还有大量错误是TypeScript在编译时无法捕获的,例如堆栈溢出、网络断连和恶意的用户输入,这些属于运行时异常。TypeScript所能做的是把纯JavaScript代码中那些运行时错误提前到编译时报告 :::
3. 类型全解

TypeScript的类型层次结构
3.2 类型浅谈
3.2.3 boolean
let a = true // boolean ts推导值的类型为booleanvar b = false // boolean ts推导值的类型为booleanlet b: boolean = true // boolean 显式注解,告诉ts,值的类型booleanconst c = true // true ts推导出字面量类型,赋值后无法修改let e: true = true // true 显式注解,告诉ts,限定了e只能在所有布尔值中只能取true
类型字面量:仅表示一个值的类型(上述的4和5行)
3.2.8 对象
// 显式注解let a: {b: number // 必须定义c?: string // 可选[key: number]: boolean // 索引签名} = {b: 1}// 不显式注解let a = {b: 'x'}// 上述两种均为对象字面量句法
TypeScript中声明对象类型有四种方式
- 对象字面量表示法({a: string})
- 空对象字面量表示法({})。不推荐
- object类型。如果需要一个对象,但对对象的字段没有要求,使用这种方式
- Object类型。不推荐
3.2.10 数组
let a = [1, 2, 3] // number[]var b = ['a', 'b'] // string[]let c: string[] = ['a'] // string[] 显式注解let d = [1, 'a'] // (string | number)[]const e = [2, 'b'] // (string | number)[]let g = [] // any[]
3.2.11 元祖
元祖(Tuple)是array的子类型,允许表示一个已知元素数量和类型的数组,各元素的类型不必相同,声明元祖必须显式注解
let a: [number, string] = [1, 'a']let list: [string, ...string[]] = ['sara', 'tali'] //至少有一个字符串元素let list1: [number, boolean, ...string[]] = [1, true, 'b'] // 前面两个元素必须定义
只读数组和元组
// 数组type A = readonly string[] // readonly string[]type B = ReadonlyArray<string> // readonly string[]type C = Readonly<string[]> // readonly string[]// 元祖type D = readonly [number, string] // readonly [number, string]type E = Readonly<[number, string]> // readonly [number, string]
4. 函数
4.1.1 可选和默认参数
function log(message: string, userId = 'id') { // 默认参数 普通类型让TS自己推导console.log(message, userId)}type Context = {appId?: stringuserId?: string}function log1(message: string, context: Context = {}) { // 默认参数 对象等引用类型推荐注解console.log(message, context.appId)}
:::tips
- 非默认参数大部分情况显式注解比较好,因为函数体内往往会进行运算等一系列操作,TS多数情况下无法推导出参数的类型
- 参数显式注解了,函数的返回值可以不显式注解,让TS自动推导,避免重复劳动(当然如果你明确知道函数的返回类型,也可以注解吧~) :::
4.1.4 注解this的类型
function fancyDate(this: Date) {return this.getDate()}
:::info
- 如果函数使用this,请在函数的第一个参数中声明this类型。this不是常规参数,而是保留字,是函数签名的一部分
注意:开启strict会自动启用noImplicitThis(强制显示注解函数中this的类型,不强制类或对象的函数注解this) :::
4.1.7 调用签名
函数的调用签名只包含类型层面的代码,即只有类型,没有值。因此,函数的调用签名可以表示参数的类型、this的类型、返回值的类型、剩余参数的类型和可选参数的类型,但是无法表示默认值(因为默认值是值,不是类型)。调用签名没有函数的定义体,无法推导出返回类型,所以必须显式注解
// 简写调用签名type Log = (message: string, user?: string) => void// 完整调用签名type Log = {(message: string, user?: string): void}let log: Log = (message, user = 'testUser') => {....}// 上述两种写法完全等效,首选简写形式
:::info
使用调用签名的好处就是可以明确体现函数的具体类型,这一点是普通函数做不到的
:::
4.1.9 重载
简单讲就是函数的输出类型取决于参数的输入类型
type CreateElement = {(tag: 'canvas'): HTMLCanvasElement(tag: 'table'): HTMLTableElement(tag: string): HTMLElement // 兜底,类似any}// 函数表达式let createElement: CreateElement = (tag: string): HTMLElement => {....}// 函数声明function createElement(tag: 'canvas'): HTMLCanvasElementfunction createElement(tag: 'table'): HTMLTableElementfunction createElement(tag: string): HTMLElement {if(tag === 'canvas') {return document.createElement(tag)} else if(tag === 'table') {return document.createElement(tag)} else {// ...}}
5. 类和接口
5.2 类
5.4 接口
与类型别名相似,接口是一种命名类型的方式,这样就不用在行内定义了(即定义与实现分离)。
两者算是同一概念的句法,下面列出共同点与不同点
共同点
// 类型别名type Sushi = {calories: numbersalty: boolean}// 接口interface Sushi = {calories: numbersalty: boolean}
// 类型别名type Food = {calories: numbertasty: boolean}type Sushi = Food & { // 交集,3个属性都需定义实现salty: boolean}type Cake = Food & {sweet: boolean}// 接口interface Food = {calories: numbertasty: boolean}interface Sushi extends Food { // 继承,3个属性都需定义实现salty: boolean}interface Cake extends Food {sweet: boolean}
不同点
- 类型别名更为通用,右边可以是任何类型;而在接口声明中,右边必须为结构 ```typescript type A = number type B = A | string
interface … = {
}
2. 扩展接口时,TypeScript将检查扩展的接口是否可赋值给被扩展的接口```typescriptinterface A {good(x: number): stringbad(x: number): string}interface B extends A {good(x: string | number): stringbad(x: string): string // Error TS....}// 接口换成类型别名,extends换成交集&不会出现这种问题
- 同一作用域中的多个同名接口将自动合并(声明合并);而多个同名类型别名将导致编译错误。
:::info
总结
type -> inteface -> class (复杂度及丰富等级)
- 简单结构推荐使用type
- 中等复杂结构推荐使用interface(接口使用的最多)
- 跟业务相关类或结构扩展性更强的推荐使用class :::
6. 类型进阶
6.6 解决办法
6.6.1 类型断言
明确告诉TS我知道类型
// 尖括号语法 (JSX语法不支持)let someValue: any = "this is a string";let strLength: number = (<string>someValue).length;// 推荐使用as语法let someValue: any = "this is a string";let strLength: number = (someValue as string).length;
6.6.2 非空断言
如果编译器不能够去除 null或 undefined,你可以使用非空断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined
6.6.3 明确赋值断言
let userId: stringfetchUser()userId.toUpperCase() // Error....function fetchUser() {userId = globalCache.get('userId')}// 改进let userId!: string // 加了!,明确告诉ts在读取userId时,肯定已经赋值了fetchUser()userId.toUpperCase()function fetchUser() {userId = globalCache.get('userId')}
:::tips 总结:断言语句应尽可能少使用,如果经常使用,可能表明你的代码有问题 :::
7. 异常
// error.tsexport class InvalidDateFormatError extends RangeError {}export class DateIsInTheFutureError extends RangeError {}
import { InvalidDateFormatError, DateIsInTheFutureError } from './error'function isVaild(data: Date) {return Object.prototype.toString.call(date) === '[object Date]'&& !Number.isNaN(date.getTime())}function parse(birthday: string): Date {let date = new Date(birthday)if(!isVaild(date)) {throw new InvalidDateFormatError('Enter a date in the form YYYY/MM/DD')}if(date.getTime() > Date.now()) {throw new DateIsInTheFutureError('Are you a timelord?')}return date}try{let date = parse(ask())} catch(e) {if(e instanceof InvalidDateFormatError) {console.error(e.message)} else if (e instanceof DateIsInTheFutureError) {console.info(e.message)} else {throw e}}
9. 前后端框架
9.1 前端框架
// tsconfig.json{"compilerOptions": {"lib": ["dom"], // 项目中使用DOM API"allowSyntheticDefaultImports": true, // 可使用默认导入语法 import React from "react""jsx": "react" // 开启TSX(JSX + TypeScript)}}
9.1.1 React
函数式组件
import React from 'react'type Props = { // 也可以用interfaceisDisabled?: booleansize: 'Big' | 'Small'onClick(event: React.MouseEvent<HTMLButtonElement>): void}export function FancyButton(props: Props) {const [toggled, setToggled] = React.useState(false) // Hookreturn <buttonclassName = {props.size}disabled = {props.isDisabled || false}onClick = {event => {setToggled(!toggled)props.onClick(event)}}>{props.size}</button>}let button = <FancyButtonsize='Big'onClick = {() => {console.log('clicked')}}/>
类组件
import React from 'react'import { FancyButton } from './FancyButton'type Props = {firstName: stringuserId: string}type State = {isLoading: boolean}class SignupForm extends React.Component<Props, State> {state = {isLoading: false}private signUp = async() => {this.setState({ isLoading: true })try{await fetch('/api/sign...' + this.props.userId)} finally {this.setState({ isLoading: false })}}render() {return (<><FancyButtonisDisabled={this.state.isLoading}size='Big'onClick={this.signUp}/></>)}}let form = <SignupForm firstName='name' userId='13ab' />
:::tips
- 事件类型参考event
- 更多用法可参考typescript-cheatsheets :::
11. 与JavaScript互操作
11.3 寻找JavaScript代码的类型信息
- 在TypeScript文件中导入JavaScript文件
- 在同一级目录中寻找与.js文件同名的.d.ts文件
- 如果不存在这样的文件,而且
allowJs和checkJs的值为true,推导.js文件的类型信息 - 如果无法推导,把整个模块视作any
- 在TypeScript文件中导入npm包
- 在本地寻找模块的类型声明,找到就用 (
type.d.ts) - 本地找不到,再分析模块
package.json,如果文件中定义了名为types或typings的字段,使用字段设置的.d.ts文件做为模块的类型声明源 - 如果没有上述字段,沿着目录结构逐级向上,寻找
node_modules/@types文件夹
- 在本地寻找模块的类型声明,找到就用 (

(import React from ‘react’ 会找到@types/react/index.d.ts)
// tsconfig.json{"compilerOptions": {"typeRoots": ["./typings", "./node_modules/@types"], //修改寻找全局类型声明的默认行为 (默认是type.d.ts)"types": ["react"] // 忽略所有第三方类型声明从types中找,react除外}}
11.4 使用第三方JavaScript
使用NPM在项目中安装的第三方库大致分为三种情况
- 安装的代码自带类型声明
- 安装的代码没有自带类型声明,不过DefinitelyTyped中有相应的类型声明
- 安装的代码没有自带类型声明,而且DefinitelyTyped中也没有相应的类型声明
11.4.1 自带类型声明的JavaScript
11.4.2 DefinitelyTyped中有类型声明的JavaScript
DefinitelyTyped(类似一个社区),为众多开源项目提供外参模块声明,DefinitelyTyped中的所有类型声明都已发布到NPM中,放在@types作用域下,可以放心使用,检查你的安装的包在DefinitelyTyped中有没有类型声明
11.4.3 DefinitelyTyped中没有类型声明的JavaScript
在导入的文件上加上
@ts-ignore指令,全部内容是any类型// @ts-ignoreimport module from 'untyped-module'
创建一个空白的类型声明文件,新建一个类型声明文件(type.d.ts),写入下述外参类型声明,全是
any类型// type.d.tsdeclare module 'nearby-ferret-alerter'
自己编写外参模块声明…
- 自己编写类型声明,发布到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} | 极好 |
