Skip to content

TypeScript ​

01. 类型的重要性 ​

类型的思维方式在函数式和面向对象是高度统一的。

关于类型的思考 ​

1. 分类思维:什么是什么?

  • Integer/Strings is Comparable
  • Array is Enumerable
  • React.ComponentType is not React.ComponentClass
typescript
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>0
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>0

2. 类型映射

  • 可以转换:ComponentClass => Component
  • 因此: Array<ComponentClass> => Array<Component />

3. 类型演化

  • 开始时:React 组建是 ComponentClass,现在是 ElementType,因为 ElementType 不仅仅有 ComponentClass
  • 开始时:Skedo 渲染的基本单位是 Node,后来是 Node | LinkedNode,再后来是 NodeType

通过类型的演进不断让系统进化(领域驱动开发最核心的理念)。

类型是检查利器 ​

每次类型检查都可以帮助减少程序错误。

设计的好的类型系统可以做到编译时检查通过既可上线,虽然可能仍存在一些小问题。

总结 ​

  • 类型是人的思考方式,类型可以帮助人思考(好的程序设计可以帮助人更好的思考);
  • 类型可以帮助检查错误(减少 90% 以上的运行时 bug,编写完即上线);
  • 类型可以帮助系统演进。

02. TS 介绍 ​

2012 年微软发布的一门编程语言。发布时就提供了将 TypeScript 翻译到 JS 的翻译编译器(transcompiler)。

  • TypeScript 是 JS 的一个超集;
  • TS 同时支持 Client Side 和 Server Side;
  • TS 是一个多范式语言;
  • TS 同时支持 Duck Typing,Gradual Typing 和 Strict Typing。

学习目标

  • 理解 TS 的原理(编译时、运行时)
  • 掌握常见用法
    • 阅读源码水平
    • 编写 TS 程序
  • 能够帮助团队成员配置 TS
  • 熟悉 TS 生态

如果你不能给团队成员讲 TS,那就是没有学会。

03. 环境配置 ​

基础配置 ​

概要 ​

  • ts-node
  • tsc
  • t s config.json
  • vscode
  • 和 webpack 搭配使用

ts-node ​

Node 环境的 typescript 解释执行器。REPL(Read eval print loop)。

bash
npm i -g ts-node
# yarn global add ts-node
npm i -g ts-node
# yarn global add ts-node

使用 ts-node 执行文件

bash
ts-node smoe.ts
ts-node smoe.ts

配置文件 ​

tsconfig.json

json
{
  "compilerOptions": {}
}
{
  "compilerOptions": {}
}

tsc ​

一个 ts 的编译器。

bash
npm i -g tsc
# yarn global add tsc
npm i -g tsc
# yarn global add tsc

可以指定编译某个 ts 文件:

bash
tsc hello.ts
tsc hello.ts

也可以通过 tsconfig.json 配置。

  • 可以用 outDir 配置项配置 js 文件输出的位置;
  • tsc 作为一个指令,可以用 --help 查看用法;
  • 可以用 module 指定生成模块的类型。

当存在 tsconfig.json 时,可以在项目根目录下可以直接运行 tsc 命令对文件进行编译。

json
{
  "compilerOptoins": {
    "outDir": "dist"
  }
}
{
  "compilerOptoins": {
    "outDir": "dist"
  }
}

与 webpack 搭配使用 ​

初始化项目

bash
mkdir ts-webpack
cd ts-webpack
npm init -y
mkdir ts-webpack
cd ts-webpack
npm init -y

安装依赖

bash
npm install webpack webpack-cli ts-loader typescript --save-dev
# yarn add webpack webpack-cli ts-loader typescript
npm install webpack webpack-cli ts-loader typescript --save-dev
# yarn add webpack webpack-cli ts-loader typescript

编写测试文件

typescript
// src/index.ts

export class TreeNode<T> {
  left: TreeNode<T>
  right: TreeNode<T>
  data: T

  constructor(data: T) {
    this.data = data
  }
}

function log(x) {
  console.log(x)
}

const node = new TreeNode<number>(100)

log(node.data)
// src/index.ts

export class TreeNode<T> {
  left: TreeNode<T>
  right: TreeNode<T>
  data: T

  constructor(data: T) {
    this.data = data
  }
}

function log(x) {
  console.log(x)
}

const node = new TreeNode<number>(100)

log(node.data)

编写 webpack 配置文件

js
// webpack.config.js

const path = require('path')

module.exports = {
  entry: {
    index: './src/index.ts'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.ts/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
// webpack.config.js

const path = require('path')

module.exports = {
  entry: {
    index: './src/index.ts'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.ts/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}

编写 npm 脚本

json
{
  "name": "ts-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start:dev": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "ts-loader": "^9.4.2",
    "typescript": "^4.9.5",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1"
  }
}
{
  "name": "ts-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start:dev": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "ts-loader": "^9.4.2",
    "typescript": "^4.9.5",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1"
  }
}

运行测试脚本

bash
yarn start:dev
node dist/bundle.index.js
yarn start:dev
node dist/bundle.index.js

进阶配置 ​

概要 ​

  • react + ts-loader 配置
  • react + babel 配置
  • vue + vue-loader 配置
  • vue + babel 配置

核心问题:preset 的顺序有关系吗?preset 的作用是什么?

react + ts-loader ​

安装 npm 包

bash
pnpm i react react-dom
pnpm i react react-dom
bash
pnpm i @types/react @types/react-dom --save-dev
pnpm i @types/react @types/react-dom --save-dev
bash
pnpm i awesome-typescript-loader --save-dev
pnpm i awesome-typescript-loader --save-dev

ts-loader 已经安装过,这里不再安装。

bash
pnpm i webpack-dev-server html-webpack-plugin -D
pnpm i webpack-dev-server html-webpack-plugin -D

编写 react 文件

tsx
// src/ReactHello.tsx

import React from 'react'
import ReactDOM from 'react-dom'

const App: () => JSX.Element = () => {
  return (
    <div>
      <h1>Hello React!</h1>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
// src/ReactHello.tsx

import React from 'react'
import ReactDOM from 'react-dom'

const App: () => JSX.Element = () => {
  return (
    <div>
      <h1>Hello React!</h1>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

编写 tsconfig.json 文件

json
{
  "compilerOptions": {
    "esModuleInterop": true, // 支持多套编码风格
    "jsx": "react" // react, div => React.createElement
  }
}
{
  "compilerOptions": {
    "esModuleInterop": true, // 支持多套编码风格
    "jsx": "react" // react, div => React.createElement
  }
}

编写 template 模板文件

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

编写 webpack.react.js 文件

js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: {
    index: './src/ReactHello.tsx'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.tsx?/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    static: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'template.html')
    })
  ]
}
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: {
    index: './src/ReactHello.tsx'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.tsx?/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    static: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'template.html')
    })
  ]
}

编写测试脚本

bash
"scripts": {
  "start:dev": "webpack",
  "start:react": "webpack --config webpack.react.js"
}
"scripts": {
  "start:dev": "webpack",
  "start:react": "webpack --config webpack.react.js"
}

运行测试脚本

bash
yarn start:react
yarn start:react

这样运行命令并不会启动开发服务器,我们需要修改 scripts 脚本。

bash
"scripts": {
  "start:dev": "webpack",
  "start:react": "webpack server --config webpack.react.js"
}
"scripts": {
  "start:dev": "webpack",
  "start:react": "webpack server --config webpack.react.js"
}

重新运行脚本,就可以看到开发服务器被启动,可以正常访问。

react + babel preset ​

babel-preset 和 ts-loader 的区别是什么?

  • babel 作用

    • The Compiler for next generation Javascript
    • 所有编译 JS 的事情,babel 都干
    • es6 => es5 => es3 => polyfill
  • 缓存 + 优化

  • 插件 + 生态

两者本质区别不大,不过从生态和优化上来看,babel 更优。

babel-loader 背后是 babel,ts-loader 背后是 tsc 编译器。

bash
pnpm i babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript --save-dev
pnpm i babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript --save-dev

Babel 有两套机制,一套是 preset,一套是 plugin。我们可以认为 preset 是 plugin 的集合,其实就是预选项的概念。

编写 webpack.react.withbabel.js

js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
	// ...
  module: {
    rules: [
      {
        test: /\.tsx?/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-typescript',
              '@babel/preset-react', // 不使用 ts-loader 的情况下,需要使用
              '@babel/preset-env'
            ]
          }
        },
        exclude: /node_modules/
      }
    ]
  },
	// ...
}
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
	// ...
  module: {
    rules: [
      {
        test: /\.tsx?/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-typescript',
              '@babel/preset-react', // 不使用 ts-loader 的情况下,需要使用
              '@babel/preset-env'
            ]
          }
        },
        exclude: /node_modules/
      }
    ]
  },
	// ...
}

编写 scripts 脚本

json
{
   "scripts": {
    "start:dev": "webpack",
    "start:react": "webpack server --config webpack.react.js",
    "start:babel": "webpack server --config webpack.react.withbabel.js"
  }
}
{
   "scripts": {
    "start:dev": "webpack",
    "start:react": "webpack server --config webpack.react.js",
    "start:babel": "webpack server --config webpack.react.withbabel.js"
  }
}

vue + loader ​

增加 vue 依赖

bash
pnpm i vue@next -D
pnpm i @vue/compiler-sfc -D

pnpm i vue-loader -D
pnpm i vue@next -D
pnpm i @vue/compiler-sfc -D

pnpm i vue-loader -D

sfc:Single File Component,单文件组件

编写 vue sfc 及 bootstraper

vue
<template>
  <h1>Hello Vue!</h1>
</template>

<script lang="ts">
export default {
  setup() {
    return {}
  }
}
</script>
<template>
  <h1>Hello Vue!</h1>
</template>

<script lang="ts">
export default {
  setup() {
    return {}
  }
}
</script>
typescript
// src/main.ts

import { createApp } from 'vue'
import HelloVue from './VueHello.vue'

createApp(HelloVue).mount('#root')
// src/main.ts

import { createApp } from 'vue'
import HelloVue from './VueHello.vue'

createApp(HelloVue).mount('#root')
typescript
// src/shims-vue.d.ts

/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
// src/shims-vue.d.ts

/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

vue 需要增加 shims-vue.d.ts 文件,否则 main.ts 中引入 .vue 文件会报错。

shim(垫片),通常为了处理兼容而命名。上面这个 shim 的目标是让 vscode 和 webpack 知道 .vue 的文件可以被当作一个组件定义文件来使用。

编写 webpack.vue.js 文件

js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  entry: {
    index: './src/main.ts'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.tsx?/,
        loader: 'ts-loader',
        options: {
          // 无法识别 vue 转换的 ts 文件,需要配置该规则添加后缀
          appendTsSuffixTo: [/\.vue$/]
        },
        exclude: /node_modules/
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    static: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'template.html')
    }),
    new VueLoaderPlugin()
  ]
}
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  entry: {
    index: './src/main.ts'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.tsx?/,
        loader: 'ts-loader',
        options: {
          // 无法识别 vue 转换的 ts 文件,需要配置该规则添加后缀
          appendTsSuffixTo: [/\.vue$/]
        },
        exclude: /node_modules/
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    static: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'template.html')
    }),
    new VueLoaderPlugin()
  ]
}

编写 script 脚本

json
{
  "scripts": {
    "start:dev": "webpack",
    "start:react": "webpack server --config webpack.react.js",
    "start:babel": "webpack server --config webpack.react.withbabel.js",
    "start:vue": "webpack server --config webpack.vue.js"
  }
}
{
  "scripts": {
    "start:dev": "webpack",
    "start:react": "webpack server --config webpack.react.js",
    "start:babel": "webpack server --config webpack.react.withbabel.js",
    "start:vue": "webpack server --config webpack.vue.js"
  }
}

vue + babel preset ​

安装依赖

bash
pnpm i babel-preset-typescript-vue3 -D
pnpm i babel-preset-typescript-vue3 -D

编写 webpack.vue.withbabel.js 文件

js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  entry: {
    index: './src/main.ts'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.tsx?/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env', 'babel-preset-typescript-vue3', '@babel/preset-typescript']
        },
        exclude: /node_modules/
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    static: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'template.html')
    }),
    new VueLoaderPlugin()
  ]
}
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  entry: {
    index: './src/main.ts'
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.tsx?/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env', 'babel-preset-typescript-vue3', '@babel/preset-typescript']
        },
        exclude: /node_modules/
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: 'bundle.[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    static: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'template.html')
    }),
    new VueLoaderPlugin()
  ]
}

编写 script 脚本文件

json
{
  "scripts": {
    "start:dev": "webpack",
    "start:react": "webpack server --config webpack.react.js",
    "start:react-babel": "webpack server --config webpack.react.withbabel.js",
    "start:vue": "webpack server --config webpack.vue.js",
    "start:vue-babel": "webpack server --config webpack.vue.withbabel.js"
  }
}
{
  "scripts": {
    "start:dev": "webpack",
    "start:react": "webpack server --config webpack.react.js",
    "start:react-babel": "webpack server --config webpack.react.withbabel.js",
    "start:vue": "webpack server --config webpack.vue.js",
    "start:vue-babel": "webpack server --config webpack.vue.withbabel.js"
  }
}

技术拓展 ​

Babel preset 执行顺序有关系吗?

存在顺序关系,真正执行时可能存在多种执行顺序。

preset-react 和 preset-vue 的作用是什么?

@babel/preset-react:集成更多的 plugin,让我们可以使用,其实就是一个 plugin 包。

Preset-typescript-vue3 只增加了 typescript 的转换能力。

实战建议

实际开发时,还是推荐使用官方脚手架 react-create- app、vue 或者 vite。

我们学习环境配置只是作为架构师的一项基本技能。当你架构项目时,我们需要的是一个成熟的脚手架,推荐使用第三方的工具。

04. 日常类型 ​

基础类型 ​

string, number, boolean, null, undefined

数组类型 ​

Array<T> ,T 代表数组中的元素类型。

为什么要求数组中元素类型统一?优势是什么?

这个优势其实在于我们百分之99的需求,我们使用数组的时候,通常也是一个类型。

typescript
function useState(x) {
  return [x, setState]
}
function useState(x) {
  return [x, setState]
}

这种情况我们才会有数组中不同类型的需求,但是这样本身也不是当作数组来用。在编程领域这样叫做记录,是当作值类型在用。

any/unknown/noImplictAny ​

typescript
let obj: any = { x: 0 }
// any 屏蔽了所有类型检查,相当于你对程序的理解是高于 TS,不需要检查

obj.foo()
obj()
obj.bar = 100
obj = 'hello'
const n: number = obj
let obj: any = { x: 0 }
// any 屏蔽了所有类型检查,相当于你对程序的理解是高于 TS,不需要检查

obj.foo()
obj()
obj.bar = 100
obj = 'hello'
const n: number = obj

Implict:隐式、explict:显式

配置项:noImplictAny,建议开启。

  • 当不给变量声明类型时,如果 noImplictAny = false,那么他就是 any;
  • 如果 noImplictAnt = true,就会报错。
typescript
function foo(x) {
	console.log(x)
}

// =>

function foo(x: number) {
  console.log(x)
}
function foo(x) {
	console.log(x)
}

// =>

function foo(x: number) {
  console.log(x)
}

起初 ts 中没有 unknown 类型,是后来新加的类型。

typescript
let x: unknown = 1

x = true

let y: string = x // 不能将类型“unknown”分配给类型“string”。
let x: unknown = 1

x = true

let y: string = x // 不能将类型“unknown”分配给类型“string”。

通常情况下,我们需要使用 any 的地方,使用 unknown 就足够了。

为什么要提供 unknown 类型?

unknown 其实是 any 的替代品,它是一个类型安全的 any 的替代品。unknown 的值可以随便设置,但是将 unknown 赋值给别的值就会报错。 把一个 unknown 赋值给其他值本身也是一个比较危险的事情。

类型标注 ​

使用 : 用于类型标注。

typescript
let myName01: string = 'Alice'
let myName02 = 'Alice' // 类型推断是 string
let myName01: string = 'Alice'
let myName02 = 'Alice' // 类型推断是 string

函数 ​

typescript
function greet(name: string): number {
  console.log(`Hello, ${name.toUpperCase()} !!`)
}
greet(42) // Error

let x: string = greet('omg') // Error
function greet(name: string): number {
  console.log(`Hello, ${name.toUpperCase()} !!`)
}
greet(42) // Error

let x: string = greet('omg') // Error
typescript
const names = ['Alice', 'Bob', 'Eve']
names.forEach(s => {
  console.log(s.toUpperCase())
  // typescript 会根据上下文类型推测类型
})
const names = ['Alice', 'Bob', 'Eve']
names.forEach(s => {
  console.log(s.toUpperCase())
  // typescript 会根据上下文类型推测类型
})
typescript
function print(arg1: string, arg2?: string) {
  console.log(arg1, arg2)
}

print('hello', 'world')
print('hello')
function print(arg1: string, arg2?: string) {
  console.log(arg1, arg2)
}

print('hello', 'world')
print('hello')

对象类型 ​

对象如果描述类型也需要严格执行。

typescript
const pt: {
  x: number
  y: number
} = { x: 100, y: 100 }

pt.z = 1998 // Error
const pt: {
  x: number
  y: number
} = { x: 100, y: 100 }

pt.z = 1998 // Error

可选项:

typescript
function printName(obj: { first: string; last?: string }) {}

printName({ first: 'Bob' })
printName({ first: 'Alice', last: 'Alisson' })
function printName(obj: { first: string; last?: string }) {}

printName({ first: 'Bob' })
printName({ first: 'Alice', last: 'Alisson' })

? 表达式:? 代表可能是 undefined,但是安全很多。

typescript
const o: {
  a: string
  b?: {
    c: string
  }
} = { a: '1' }

// console.log(o.b.c) // Cannot read properties of undefined (reading 'c')
console.log(o.b?.c) // undefined

o.b.c = 'Hello' // Error
const o: {
  a: string
  b?: {
    c: string
  }
} = { a: '1' }

// console.log(o.b.c) // Cannot read properties of undefined (reading 'c')
console.log(o.b?.c) // undefined

o.b.c = 'Hello' // Error

联合类型 ​

所谓联合类型,就是多个类型组合成一个新的类型。

| 取自于 IFSTMT,即 if {} | if {} else {},其实是或的关系。

typescript
function printId(id: number | string) {
  console.log("your id is:" + id)
}

printId(101)
printId("202")
printId({ id: 303 }) // Error
function printId(id: number | string) {
  console.log("your id is:" + id)
}

printId(101)
printId("202")
printId({ id: 303 }) // Error

联合类型只能使用两个类型的公共操作。

typescript
function printId(id: number | string) {
  console.log(id.toUpperCase())
  // 类型“string | number”上不存在属性“toUpperCase”。类型“number”上不存在属性“toUpperCase”。
}
function printId(id: number | string) {
  console.log(id.toUpperCase())
  // 类型“string | number”上不存在属性“toUpperCase”。类型“number”上不存在属性“toUpperCase”。
}

ts 会针对联合类型做排除法,其实这是类型守卫的一种实现,用于窄化类型。

typescript
function printId(id: number | string) {
  if (typeof id === 'number') {
    console.log(id)
    return
  }
  console.log(id.toUpperCase())
}
function printId(id: number | string) {
  if (typeof id === 'number') {
    console.log(id)
    return
  }
  console.log(id.toUpperCase())
}

类型别名 ​

别名可以理解成字符串的查找和替换。

typescript
type Point = {
  x: number
  y: number
}

function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x)
  console.log("The coordinate's y value is " + pt.y)
}

printCoord({ x: 100, y: 100 })
type Point = {
  x: number
  y: number
}

function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x)
  console.log("The coordinate's y value is " + pt.y)
}

printCoord({ x: 100, y: 100 })

类型别名也可以使用联合:

typescript
type ID = number | string
type ID = number | string

别名只是 “别名”,不是一种类型:

typescript
let x: ID = 100
typeof x // number
let x: ID = 100
typeof x // number

别名也可以和它代表的类型一起工作(别名并不是创建新的类型)

typescript
let id: ID = 'abc'
id = 456 // ok
let id: ID = 'abc'
id = 456 // ok

接口 ​

typescript
interface Point {
  x: number
  y: number
}

function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x)
  console.log("The coordinate's y value is " + pt.y)
}

printCoord({ x: 100, y: 100 })
interface Point {
  x: number
  y: number
}

function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x)
  console.log("The coordinate's y value is " + pt.y)
}

printCoord({ x: 100, y: 100 })

interface 与 type 有很多相似之处,但是它作为一个接口,也承担着一些特殊的含义在里面。

typescript
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear = getBear()

bear.name
bear.honey

// ----------

type Animal = {
  name: string
}

type Bear = Animal & {
  honey: boolean
}

const bear = getBear()

bear.name
bear.honey
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear = getBear()

bear.name
bear.honey

// ----------

type Animal = {
  name: string
}

type Bear = Animal & {
  honey: boolean
}

const bear = getBear()

bear.name
bear.honey

两者看起来实现效果基本一样,但是在语义上是不同的,interface 是继承,type 是类型组合。

接口的声明合并(Declaration Merging)

typescript
interface Box {
  width: number
  height: number
}
interface Box {
  scale: number
}

const box: Box = { width: 5, height: 10, scale: 3 }
interface Box {
  width: number
  height: number
}
interface Box {
  scale: number
}

const box: Box = { width: 5, height: 10, scale: 3 }

如果接口被定义两次并不是覆盖,而是合并的关系。

类型断言(assertion) ​

tsx
const canvas = document.getElementById("main_canvas") as HTMLCanvasElement
const canvas = document.getElementById("main_canvas") as HTMLCanvasElement

通常 ts 会接收 “说的通” 的类型断言,但是有的类型断言 ts 会拒绝。

typescript
const x = "hello" as number
// 类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。
// 如果这是有意的,请先将表达式转换为 "unknown"。
const x = "hello" as number
// 类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。
// 如果这是有意的,请先将表达式转换为 "unknown"。

你可以使用 any as T 来 “欺骗” TS:

typescript
const a = (expr as unknown) as T
const a = (expr as unknown) as T

as 并不是类型的转化,其本质还是类型断言。

字面类型 ​

对于常量,在 TS 中实际上是 Literal Type。

typescript
const someStr = "abc"
// someStr 的类型是 “abc”,它的值只能是 abc

const foo = 1
// foo 的类型是 1(不代表是整数)

// 对于 ts 理解是这样,如果我们使用 typeof 操作符也可以得到正确的结果
// typeof someStr // 'string'
// typeof foo // 1

// 对于 let
let foo = 1 // foo: number
const someStr = "abc"
// someStr 的类型是 “abc”,它的值只能是 abc

const foo = 1
// foo 的类型是 1(不代表是整数)

// 对于 ts 理解是这样,如果我们使用 typeof 操作符也可以得到正确的结果
// typeof someStr // 'string'
// typeof foo // 1

// 对于 let
let foo = 1 // foo: number

const 和 let 对于 ts 来说,其实是区分对待的,const 定义的数据对于 ts 来说其实就是一种类型,所以不能修改。

我们可以用字面类型来约束一些特殊的函数,比如:

typescript
function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1
}
function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1
}

下面是一个更贴近真实场景的案例:

typescript
interface Options {
  width: number
}

function configure(x: Options | 'auto') {
  // ...
}

configure({ width: 100 })
configure('auto')
configure('automatic') // 类型“"automatic"”的参数不能赋给类型“Options | "auto"”的参数。
interface Options {
  width: number
}

function configure(x: Options | 'auto') {
  // ...
}

configure({ width: 100 })
configure('auto')
configure('automatic') // 类型“"automatic"”的参数不能赋给类型“Options | "auto"”的参数。

字面类型可以起到纠正参数的作用。

字面类型的坑:

typescript
function handleRequest(url: string, method: 'GET' | 'POST') {
  // do ...
}

const req = { url: 'https://example.com', method: 'GET' }
handleRequest(req.url, req.method)
// 类型“string”的参数不能赋给类型“"GET" | "POST"”的参数。
// 这里提示是正确的,method 是字面量类型,但是我们定义的 req 中的 method 是字符串类型,而且还有可能被修改成别的值
function handleRequest(url: string, method: 'GET' | 'POST') {
  // do ...
}

const req = { url: 'https://example.com', method: 'GET' }
handleRequest(req.url, req.method)
// 类型“string”的参数不能赋给类型“"GET" | "POST"”的参数。
// 这里提示是正确的,method 是字面量类型,但是我们定义的 req 中的 method 是字符串类型,而且还有可能被修改成别的值

我们可以这样解决:

typescript
const req = { url: 'https://example.com', method: 'GET' as 'GET' }
handleRequest(req.url, req.method)
const req = { url: 'https://example.com', method: 'GET' as 'GET' }
handleRequest(req.url, req.method)

or

typescript
const req = { url: 'https://example.com', method: 'GET' }
handleRequest(req.url, req.method as 'GET')
const req = { url: 'https://example.com', method: 'GET' }
handleRequest(req.url, req.method as 'GET')

or

typescript
const req = { url: 'https://example.com', method: 'GET' } as const
handleRequest(req.url, req.method)
const req = { url: 'https://example.com', method: 'GET' } as const
handleRequest(req.url, req.method)

null/undefined ​

null 和 undefined 是 JavaScript 的两种基础类型(Primtive type),它们描述的是不同的行为:

  • undefined 是一个没有被分配值的变量
  • null 是一个被人为分配的空值

TypeScript 有一个配置项,叫做 strictNullChecks,这个配置项设置为 on 的时候,在使用有可能是 null 值时,需要显式检查。

typescript
function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log('Hello, ' + x.toUpperCase())
  }
}
function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log('Hello, ' + x.toUpperCase())
  }
}

另外,我们可以用 ! 操作符,来断言某个值不是空值:

! 代表非空断言。

typescript
function doSomething(x: string | null) {
  console.log("Hello, " + x!.toUpperCase())
}
function doSomething(x: string | null) {
  console.log("Hello, " + x!.toUpperCase())
}

枚举类型 ​

typescript
enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}
enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}

枚举类型最后会被翻译成整数,因此枚举的很多性质和整数相似。

比如 Down.toString() 会返回 2,而不是 Down。正因为如此,枚举类型的效率很高。

typescript
enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}

console.log(Direction.Up) // 1
enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}

console.log(Direction.Up) // 1

如果你想 ts 返回字符串,还可以像这样做。这样叫做反向映射。

typescript
console.log(Direction[Direction.Up]) // Up
console.log(Direction[Direction.Up]) // Up

当然如果你想用字符串类的枚举,就需要显示的为每一项赋值,不过这样意义并不大,因为 enum 已经提供了反向操作的方式。

typescript
enum Direction {
  Up = 'UP',
  Down = "Down",
  Left = 'LEFT',
  Right = 'RIGHT'
}
enum Direction {
  Up = 'UP',
  Down = "Down",
  Left = 'LEFT',
  Right = 'RIGHT'
}

我们还可以混合使用,不过这样不仅没有意义,而且还会减少代码可读性。

typescript
enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = 'YES'
}
enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = 'YES'
}

在运行时,Enum 会被解释成对象,Enum 的每项都会被解释成常数。例如下面这个例子:

typescript
enum E {
  X,
  Y,
  Z
}

function f(obj: { X: number }) {
  return obj.X
}

console.log(f(E)) // 0
enum E {
  X,
  Y,
  Z
}

function f(obj: { X: number }) {
  return obj.X
}

console.log(f(E)) // 0

可以用下面这个语法提取 Enum 中的字符串,这个也叫 Reverse Mapping。刚才我们也提到过。

typescript
console.log(E[E.X]) // X
console.log(E[E.X]) // X

05. 泛型 ​

泛型概念 ​

泛型,可以说是提取了一类事物的共性特征的一种抽象。比如说,松树、柳树都是树,在程序里有 3 种表达:

  • 接口(Interface)
  • 继承(Ineritance)
  • 泛型(Generics)

继承是一种强表达。

松树继承于树,松树同时也是木材。这样关系的表达,可以让松树多重继承(树、木材),要么松树 < 树 < 木材。

无论哪种,增加程序设计复杂度,也加强了继承关系的维护成本(或者说耦合)。这样看,关系太强,其实并不好。

接口是一种方面(Aspect)描述。比如松树可以生长,那么松树是:Growable;动植物都可以进化,那么它们是 Evolvable。

一个类型可以拥有多个方面的特性:

**泛型(Generics)**是对共性的提取(不仅仅是描述)。

typescript
class BedMaker<t> {
  make() {}
}

const A = new BedMaker<红木>()
const B = new BedMaker<桃木>()
class BedMaker<t> {
  make() {}
}

const A = new BedMaker<红木>()
const B = new BedMaker<桃木>()
  • 木头可以制造床,但不是所有的木头都可以制造床;
  • 制造床这个方法,放进木头类中就会很奇怪,因为木头不仅仅可以制造床;
  • 同理,让木头继承于 “可以制造床” 这个接口也很奇怪。
typescript
// 奇怪的代码展示

class 红木 implements IMakedBed {
  makeBed() { ... }
}
// 奇怪的代码展示

class 红木 implements IMakedBed {
  makeBed() { ... }
}

设计 IMakedBed 的目标是为了拆分描述事物不同的方面(Aspect),还有一个更专业的词汇 - 关注点(Interest Point)。拆分关注点的技巧,叫做关注点分离。如果仅仅用接口,不用泛型,那么关注点就没有做到完全解耦。

泛型是一种抽象共性(本质)的编程手段,它允许将类型作为其他类型的参数(表现形式),从而分离不同关注点的实现(作用)。

  • Array<T> 分离的是数据可以被线性访问、存储的共性。

  • Stream<T> 分离的是数据可以随着时间产生的共性。

  • Promsie<T> 分离的是数据可以被异步计算的特性。

Hello 泛型 ​

typescript
// 一个 identity 函数是自己返回自己的函数
// 可以声明它是:number - number
function identity(arg: number): number {
  return arg
}

// 为了可以让 identity 支持更多类型,可以声明它是 any
function identity(arg: any): any {
  return arg
}

// any 会丢失后续所有的检查,这时我们可以考虑使用泛型
function identity<Type>(arg: Type): Type {
  return arg
}

let output = identity<string>("MyString")
// 不同显示的指定 <> 中的类型
// let output = identity("MyString")

output = 100 // Error
// 一个 identity 函数是自己返回自己的函数
// 可以声明它是:number - number
function identity(arg: number): number {
  return arg
}

// 为了可以让 identity 支持更多类型,可以声明它是 any
function identity(arg: any): any {
  return arg
}

// any 会丢失后续所有的检查,这时我们可以考虑使用泛型
function identity<Type>(arg: Type): Type {
  return arg
}

let output = identity<string>("MyString")
// 不同显示的指定 <> 中的类型
// let output = identity("MyString")

output = 100 // Error

<> 叫做钻石操作符,代表传入的类型参数

泛型类 ​

typescript
class GenericNumber<NumType> {
  zeroValue: NumType
  add: (x: NumType, y: NumType) => NumType
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function (x, y) {
  return x + y
}

let stringNumeric = new GenericNumber<string>()
stringNumeric.zeroValue = ''
stringNumeric.add = function (x, y) {
  return x + y
}
class GenericNumber<NumType> {
  zeroValue: NumType
  add: (x: NumType, y: NumType) => NumType
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function (x, y) {
  return x + y
}

let stringNumeric = new GenericNumber<string>()
stringNumeric.zeroValue = ''
stringNumeric.add = function (x, y) {
  return x + y
}

推荐将声明(Declaration)和定义(Definition)写到一起:

typescript
class GenericNumber<T> {
  private zeroValue: T

  constructor(v: T) {
    this.zeroValue = v
  }

  public add(x: T, y: T) {
    return x + y
  }
}
class GenericNumber<T> {
  private zeroValue: T

  constructor(v: T) {
    this.zeroValue = v
  }

  public add(x: T, y: T) {
    return x + y
  }
}

泛型约束(Generic Constraints) ​

下面的程序会报错:

typescript
function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length)
  // 类型“Type”上不存在属性“length”。
  return arg
}
function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length)
  // 类型“Type”上不存在属性“length”。
  return arg
}

我们可以考虑为 arg 增加约束:

typescript
interface Lengthwise {
  length: number
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length)
  return arg
}
interface Lengthwise {
  length: number
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length)
  return arg
}

我们还可以用 keyof 关键字作为泛型约束:

typescript
type Point = { x: number; y: number }
type P = keyof Point // “x” | “y”
type Point = { x: number; y: number }
type P = keyof Point // “x” | “y”
typescript
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key]
}

const x = { a: 1, b: 2, c: 3, d: 4 }

getProperty(x, 'a')
getProperty(x, 'm') // 类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key]
}

const x = { a: 1, b: 2, c: 3, d: 4 }

getProperty(x, 'a')
getProperty(x, 'm') // 类型“"m"”的参数不能赋给类型“"a" | "b" | "c" | "d"”的参数

不过 ts 为什么可以这么做?因为对 TS 而言所有对象的 key 都是静态的。

typescript
const a = { x: 1, y: 2 }
a.z = 3 // Error
const a = { x: 1, y: 2 }
a.z = 3 // Error

因为是静态的,所以可以用 keyof 操作符求所有的 key。如果一个对象的类型是 any,那么 keyof 也就没有意义了。

实例化泛型类型(将类作为参数) ​

typescript
function create<Type>(c: { new (): Type }): Type {
  return new c()
}

create(Foo) // Foo 的实例
function create<Type>(c: { new (): Type }): Type {
  return new c()
}

create(Foo) // Foo 的实例

不错的例子:

typescript
class BeeKeeper {
  hasMask: boolean = true
}

class ZooKeeper {
  nametag: string = 'Mike'
}

class Animal {
  numLegs: number = 4
}

class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper()
}

class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper()
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c()
}

createInstance(Lion).keeper.nametag
createInstance(Bee).keeper.hasMask
class BeeKeeper {
  hasMask: boolean = true
}

class ZooKeeper {
  nametag: string = 'Mike'
}

class Animal {
  numLegs: number = 4
}

class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper()
}

class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper()
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c()
}

createInstance(Lion).keeper.nametag
createInstance(Bee).keeper.hasMask

总结 ​

什么时候用接口?什么时候用泛型?

接口是用来约束一个类型的行为;泛型实在提取某些共性,做成类似模板一样的语法,将共性抽象成泛型。

将类型作为参数传递,并实例化有哪些应用场景?

例如 React.createClass(""),其实应用场景也有很多。

06. 类型窄化 ​

TS中的类型是可以组合使用的。

联合和窄化 ​

typescript
type Padding = number | string

function padLeft(padding: Padding, input: string): string {
  // ...
}
type Padding = number | string

function padLeft(padding: Padding, input: string): string {
  // ...
}

当我们编写上述代码会遇到一个问题,我们需要用 typeof 判断 padding 的类型。

当然作为一个 number | string 的联合类型可以赋值成 number 或者 string。

typescript
let x: number | string = 1
x = 'Hello'
let x: number | string = 1
x = 'Hello'

如果不判断:

typescript
function padLeft(padding: number | string, input: string) {
  return new Array(padding + 1).join(' ') + input
  // 运算符“+”不能应用于类型“string | number”和“number”。
}
function padLeft(padding: number | string, input: string) {
  return new Array(padding + 1).join(' ') + input
  // 运算符“+”不能应用于类型“string | number”和“number”。
}

于是我们可以增加 typeof 的判断:

typescript
function padLeft(padding: number | string, input: string) {
  if (typeof padding === 'number') {
    return new Array(padding + 1).join(' ') + input
  }
  return padding + input
}
function padLeft(padding: number | string, input: string) {
  if (typeof padding === 'number') {
    return new Array(padding + 1).join(' ') + input
  }
  return padding + input
}

当进行 if + typeof 操作后,ts 可以识别变窄后的类型,称为窄化(Narrowing)。上面 Narrowing 的能力,可以让 TS 清楚的知道 padding 是数字还是字符串。

在实现层面,TS 会认为 typeof padding = "number" 。这样的表达式是一种类型守卫(type guard)表达式。 当然这是纯粹实现层面的概念,准确来说是 if + type guard 实现了 Narrowing。

类型窄化(Type Narrowing)根据类型守卫(Type Guard)在子语句块重新定义更具体的类型。

typeof 守卫 ​

typescript
// 返回值
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
// 返回值
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"

注意,因为 typeof null === 'object'。因此:

typescript
function printAll(strs: string | string[] | null) {
  if (typeof strs === 'object') {
    for (const s of strs) {
      // Object is possibly 'null'
      console.log(s)
    }
  } else if (typeof strs === 'string') {
    console.log(strs)
  } else {
    // do nothing
  }
}
function printAll(strs: string | string[] | null) {
  if (typeof strs === 'object') {
    for (const s of strs) {
      // Object is possibly 'null'
      console.log(s)
    }
  } else if (typeof strs === 'string') {
    console.log(strs)
  } else {
    // do nothing
  }
}

真值窄化 ​

真值窄化(Truthiness narrowing)。

Javascript 有一张复杂的真值表,总结下来这些值都会拥有 false 的行为:

typescript
0
NaN
"" (the empty string)
0n  (the bigint version of zero)
null
undefined
0
NaN
"" (the empty string)
0n  (the bigint version of zero)
null
undefined

我们也可以通过真值实现窄化。例如我们可以避免:TypeError:null is not iterable 错误。

typescript
function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === 'object') {
    for (const s of strs) {
      // Object is possibly 'null'
      console.log(s)
    }
  } else if (typeof strs === 'string') {
    console.log(strs)
  } else {
    // do nothing
  }
}
function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === 'object') {
    for (const s of strs) {
      // Object is possibly 'null'
      console.log(s)
    }
  } else if (typeof strs === 'string') {
    console.log(strs)
  } else {
    // do nothing
  }
}

再举个例子:

typescript
function multiplyAll(values: number[] | undefined, factor: number) {
  if (!values) {
    return values
  }
  return values.map(x => x * factor)
}
function multiplyAll(values: number[] | undefined, factor: number) {
  if (!values) {
    return values
  }
  return values.map(x => x * factor)
}

真值(Truthiness narrowing)窄化可以帮助我们更好的应对 null/undefined/0 等值。

相等性窄化 ​

在窄化中有一类隐式的窄化方法,就是想等性窄化。===,!==,== 和 != 都可以用来窄化类型。例如:

typescript
function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // s is string
  } else {
    // x is string | number
    // y is string | boolean
  }
}
function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // s is string
  } else {
    // x is string | number
    // y is string | boolean
  }
}

再看一个例子:

typescript
function printAll(strs: string | string[] | null) {
  if (strs !== null) {
    if (typeof strs === 'object') {
      for (const s of strs) {
      }
    } else if (typeof strs === 'string') {
    }
  }
}
function printAll(strs: string | string[] | null) {
  if (strs !== null) {
    if (typeof strs === 'object') {
      for (const s of strs) {
      }
    } else if (typeof strs === 'string') {
    }
  }
}
typescript
interface Container {
  value: number | null | undefined
}

function multiplyValue(container: Container, factor: number) {
  if (container.value != null) {
    container.value *= factor
  }
}
interface Container {
  value: number | null | undefined
}

function multiplyValue(container: Container, factor: number) {
  if (container.value != null) {
    container.value *= factor
  }
}

in 操作符窄化 ​

in 操作符的作用是检验对象中是否有属性。

typescript
type Fish = { swim: () => void }
type Bird = { fly: () => void }

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    return animal.swim()
  }
  return animal.fly()
}
type Fish = { swim: () => void }
type Bird = { fly: () => void }

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    return animal.swim()
  }
  return animal.fly()
}

不过我们为什么不用 instaceof Fish ?因为 type 没有运行时,代码转化后就不存在了。

instanceof 窄化 ​

instanceof 也可以做窄化,不过类型不能是 type,而是真实存在的 Function 类型。

typescript
function logValue(x: Date | string) {
  if (x instanceof Date) {
    // s is Date
  } else {
    // x is string
  }
}
function logValue(x: Date | string) {
  if (x instanceof Date) {
    // s is Date
  } else {
    // x is string
  }
}

组合类型推导 ​

有时候 TypeScript 会推导出组合类型。

typescript
let x = Math.random() < 0.5 ? 10 : 'hello world!'
let x = Math.random() < 0.5 ? 10 : 'hello world!'

这个时候 x 是 number | string。当然,这里有个问题是 number | string 的类型可以赋值成 number 或者 string。

控制流分析

TypeScript 是如何做到类型窄化的?

首先在语法分析阶段,Typescrpt 的编译器会识别出类型卫兵表达式。包括一些隐性的类型卫兵,比如真值表达式、instaceof 等等。然后在语法分析的时候,Typescript 遇到控制流关键字 if/while 等,就会看看这里有没有需要分析的窄化操作。例如:

typescript
function padLeft(padding: number | string, input: string) {
  if (typeof padding === 'number') {
    return new Array(padding + 1).join(' ') + input
  }
  return padding + input
}
function padLeft(padding: number | string, input: string) {
  if (typeof padding === 'number') {
    return new Array(padding + 1).join(' ') + input
  }
  return padding + input
}
  • 首先 ts 会看到一个卫兵表达式:typeof padding === 'number';
  • 然后 ts 会对返回值 return padding + input 以及 return new 分别做窄化;
  • 窄化的本质是重新定义类型。

很多语句都会触发窄化:

typescript
function example() {
  let x: string | number | boolean

  x = Math.random() < 0.5
  // x = boolean

  if (Math.random() < 0.5) {
    x = 'Hello'
    // x:string
  } else {
    x = 100
    // x: number
  }

  return x
  // x: string | number
}
function example() {
  let x: string | number | boolean

  x = Math.random() < 0.5
  // x = boolean

  if (Math.random() < 0.5) {
    x = 'Hello'
    // x:string
  } else {
    x = 100
    // x: number
  }

  return x
  // x: string | number
}

类型断言 ​

类型断言(Type Assertions/Predicate)。

Assertion 和 predicate 翻译过来都是断言。在计算机中,Assertion 通常是断言某个表达式的值是不是 true/false。Assertion 在很多的测试库中被使用,比如 asset.equals(a, 1) 。从语义上,这里在断言 a 的值是 1 (a === 1 是 true)。

Assertion 在说某个东西是什么。

Predicate 通常是一个函数,返回值是 true/false,比如说 list.filter(x => x.score > 500),x => s.score > 500 这个函数是一个 predicate 函数。

Prediate 是一个返回 true/false 的函数。

TS 中有两个断言操作符,Assertion 操作符 as 和 predicate 操作符 is。

as 操作符提示 TypeScript 某种类型是什么(当用户比 TypeScript 更了解类型的时候使用)。is 操作符是用户自定义的类型守卫,用于帮助 TypeScript Narrowing。

案例:

typescript
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined
}

let pet = {
  fly: () => {}
}

if (isFish(pet)) {
  // isFish 成为了 Type Guard
  pet.swim()
} else {
  pet.fly()
}
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined
}

let pet = {
  fly: () => {}
}

if (isFish(pet)) {
  // isFish 成为了 Type Guard
  pet.swim()
} else {
  pet.fly()
}

pet is Fish 让 isFish 具有的窄化功能,如果不定义,会直接报错。

那么 as/is 符不符合计算机标准语言中 Assertion/Predicate 的含义?

  • as 是比较符合 assertion 的含义的
  • is 如果是一个 predicate,是应该返回一个值的。不过 pet is Fish 在这只是起到类型卫兵的作用,含义略微有些差别。

判别的联合 ​

判别的联合(Discriminated unions)。

typescript
interface Shape {
  kind: 'circle' | 'square'
  radius?: number
  sideLength: number
}

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2
}
interface Shape {
  kind: 'circle' | 'square'
  radius?: number
  sideLength: number
}

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2
}

这样判断有什么问题吗?如果是下面这样呢?

typescript
function getArea(shape: Shape) {
  if (shape.kind === 'circle') {
    return Math.PI * shape.radius ** 2
  }
}
function getArea(shape: Shape) {
  if (shape.kind === 'circle') {
    return Math.PI * shape.radius ** 2
  }
}

上面这两种写法都存在问题,因为 radius 可能不存在。我们可以使用非 Null 断言操作符强制判断 radius 不能为空。

typescript
function getArea(shape: Shape) {
  if (shape.kind === 'circle') {
    return Math.PI * shape.radius! ** 2
  }
}
function getArea(shape: Shape) {
  if (shape.kind === 'circle') {
    return Math.PI * shape.radius! ** 2
  }
}

针对上面的代码其实还有优化空间。Circle 应该是一种单独的类型,Shape 可能还有 rect 等。

typescript
interface Circle {
  kind: 'circle'
  radius: number
}

interface Square {
  kind: 'square'
  sideLength: number
}

type Shape = Circle | Square

function getArea(shape: Shape) {
  if (shape.kind === 'circle') {
    // Narrowing
    return Math.PI * shape.radius ** 2
  }
}
interface Circle {
  kind: 'circle'
  radius: number
}

interface Square {
  kind: 'square'
  sideLength: number
}

type Shape = Circle | Square

function getArea(shape: Shape) {
  if (shape.kind === 'circle') {
    // Narrowing
    return Math.PI * shape.radius ** 2
  }
}

再整理下:

typescript
function getArea(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sideLength ** 2
  }
}
function getArea(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sideLength ** 2
  }
}

never 类型 ​

Never,即不应该出现的意思。Never 类型代表一个不应该出现的类型。因此对 Never 的赋值,都会报错。

比如下面处理 default 的逻辑:

typescript
function getArea(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sideLength ** 2
    default:
      const _exhaustiveCheck: never = shape
      // Type ... is not assignable to type never
      return _exhaustiveCheck
  }
}
function getArea(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sideLength ** 2
    default:
      const _exhaustiveCheck: never = shape
      // Type ... is not assignable to type never
      return _exhaustiveCheck
  }
}

总结 ​

类型窄化解决了联合类型校验的问题。TS 是 JS 的超集,TS 会尽量避免新增特性。

07. React Latest ​

React 从 16 开始,不断给我们眼前一亮的新特性,例如:

  • Fiber
  • React Hooks
  • Suspense
  • React 17 对渲染的启发性优化
  • ...

React 是一个渲染引擎,所有更新的特性都是为了更好的渲染页面。所有 React 的努力,其实就是两方面的事情:

  • 改进体验
  • 提升开发效率

Fiber 是 React 16 之后对体验改进的核心;React Hooks 是 16 之后对工程效率提升的核心。

React Hooks 介绍 ​

React Hooks 可以从两个方面理解:

  • 作用:
    • 让 React 更好的拥抱函数式
    • 更好的解决组合问题(关注点分离)
  • 工作原理:
    • 从原理上看它们是钩子(hook),当 React 生命周期发生变化的时候,会触发它们。

作用角度 ​

首先是 Hooks 出现之前 Class 风格的 React 组件:

jsx
class Foo extends Component {
  constructor() {
    this.sttate = {}
  }

  componentDidMount() {}
  shouldComponentUpdate() {}

  render() {
    return <div></div>
  }
}
class Foo extends Component {
  constructor() {
    this.sttate = {}
  }

  componentDidMount() {}
  shouldComponentUpdate() {}

  render() {
    return <div></div>
  }
}

Hooks 出现后,我们可以将 React 组件看作是一个函数:

jsx
function Foo() {
  return <div></div>
}
function Foo() {
  return <div></div>
}

没有生命周期函数,使用 Hooks 去替代。例如下面的累加器案例:

tsx
function Foo() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    timer(0, 1000)
      .subscribe(i => setCount(i))
  }, [])

  return <div>{count}</div>
}
function Foo() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    timer(0, 1000)
      .subscribe(i => setCount(i))
  }, [])

  return <div>{count}</div>
}

上面这段程序中没有 class,只有函数。这是一种编程风格的变化:

  • 组件是一个用来渲染的纯函数(重新定义):不再关注生命周期,不需要理解生命周期,更接近于 React compoent = f(data) 的定义;
  • 细化解决用户痛点:针对状态、作用、上下文、缓存等方面,为用户量身定做 hook 函数;而不是像之前都需要用户自己在类中实现程序;
  • 让用户以最小的代价实现关注点分离。

关注点分离的例子:

tsx
function HomePage() {
  const [productList, load] = useService('product').get()
  const [ads, loadAds] = useService('ads').get()
  const [suppestions, loadSuggs] = useService('suggestion').get()

  return <>
    <TitleBar />
    <ProductList list={productList} loadFN={load} />
    <Ads ads={ads} />
    <Suggestions list={suppestions} />
    <FootBar />
  </>
function HomePage() {
  const [productList, load] = useService('product').get()
  const [ads, loadAds] = useService('ads').get()
  const [suppestions, loadSuggs] = useService('suggestion').get()

  return <>
    <TitleBar />
    <ProductList list={productList} loadFN={load} />
    <Ads ads={ads} />
    <Suggestions list={suppestions} />
    <FootBar />
  </>

上面程序在一个页面中对应 3 个关注点进行分析,非常干净。

  • 产品列表
  • 广告
  • 推荐列表

useService 是一个自定义的 hook,可以由用户自己来实现。

Hooks 更好地拥抱了函数式,彻底改变了 React 的编程风格,简化了用户的理解,并且可以很好的帮助用户实现关注点分分离。

原理角度 ​

钩子(Hooks)是什么?

  • Git 的 Web hooks 是什么?
  • 杀毒软件监控操作系统的 Hooks 是什么?
  • React 的 Hooks 是什么?

Hooks 本质是一种消息机制。

Hooks 的作用是从系统外部监听某个系统内部的变化,并和某种特定事件挂钩。比如 Git 的 Web Hooks 看到 Git 有提交,就会触发一个 HTTP 请求。操作系统给的进程 Hook,看到新进程就发送一条消息,然后杀毒软件就可以获取这条消息。

Hook 的实现是两个方面:

  • 被监听的实体在特定情况下发送消息给 Hook(比如打开文件、Git 提交、新建进程 ...)
  • Hook 对象收到这种消息完成某个具体的工作(比如发送 Http 请求、开始杀毒、Hot Reload)

React Hook 一方面在 React 在某种特定状态发生变化的时候会通知 Hook,然后 Hook 再完成某个特定行为。

例如 useEffect,当 React 渲染的时候会触发这个 Hook,如果这个 Hook 的依赖发生变化,就会执行这个 Hook 上关联的函数。useState 是一个反向的 Hook,当用户设置状态变更的时候,会反向触发 React 的更新。

Hooks 是一种通知机制。

总结 ​

关于 React Hooks 的理解?

钩子是一种消息通知机制。列举 Git Hook、Web Hook、Webpack Hook 机制等概念。

函数式编程的拥抱,拥抱的特性,更好支持关注点分离等。

React 基础 ​

创建项目 ​

推荐使用 create-reacta-app 直接创建。

shell
npm i create-react-app -g
create-react-app hooks-example --template typescript
npm i create-react-app -g
create-react-app hooks-example --template typescript

函数组件 ​

函数组件的输入是 props 对象,输出是一个 JSX 对象。

一个函数组件可以这样写:

tsx
function App () {
  return <h2>Hello world !!!</h2>
}
function App () {
  return <h2>Hello world !!!</h2>
}

你可以看到 h2 是一个 JSX 类型,<h2/> 是一个 JSX Element,因此,APP 的返回值类型也是一个 JSX Element,准确来说是 React.JSX。

h2 也是一个组件,是一个 React 内置的组件,准确来说这个组件是 React.IntrinsicElements.h2。例如 Java 的 synchronized 关键字也被称为 Intrinsic Lock。

Intrinsic 内部的、External 外部的。

函数即组件。

在 React 中,可以向组件传递属性:

tsx
export default function App() {
  return <Grettings message="Hello React!!!" />
}

function Grettings({ message }: { message: string }) {
  return <h2>{message}</h2>
}
export default function App() {
  return <Grettings message="Hello React!!!" />
}

function Grettings({ message }: { message: string }) {
  return <h2>{message}</h2>
}

属性就是函数的参数,JSX=f(props)

输入决定输出叫做:纯(Pure),没有副作用(Side Effect)。

组件和列表关系 ​

组合关系:多个组件组成一个。

typescript
function ComposedComponent() {
  return (
    <>
      <A></A>
      <B></B>
    </>
  )
}

function A() {
  return <h2>A</h2>
}

const B = () => {
  return <h2>B</h2>
}
function ComposedComponent() {
  return (
    <>
      <A></A>
      <B></B>
    </>
  )
}

function A() {
  return <h2>A</h2>
}

const B = () => {
  return <h2>B</h2>
}

React 用 JSX 实现组合关系,比较直观。

数据映射成列表 ​

tsx
function List({ data }: { data: Array<string> }) {
  return (
    <ul>
      {data.map(word => (
        <li key={word}>{word}</li>
      ))}
    </ul>
  )
}

export default function App() {
  return <List data={['a', 'b', 'c']} />
}
function List({ data }: { data: Array<string> }) {
  return (
    <ul>
      {data.map(word => (
        <li key={word}>{word}</li>
      ))}
    </ul>
  )
}

export default function App() {
  return <List data={['a', 'b', 'c']} />
}

key 是 React 渲染机制的一环,不需要在属性中声明(不可以自定义名叫 key 的属性)。当 React 渲染的时候,就会调用组件函数。只要组件的 key 属性发生变化,React 就会重绘组件。如果 key 不变,且其他属性也不变,那么就不会发生重绘。

  • ['a', 'b', 'c'] => ['a', 'b', 'c', 'd'] (只有一个 li 新增,但是没有 li 重绘)
  • ['a', 'b', 'c'] => ['c', 'a'] (a,c 不重绘,删除 b)

属性或 key 发生变化才会导致组件重绘,父组件(List)重绘,子组件(li)不一定需要重绘。

容器组件 ​

有的组件是一个容器,比如说一个支持 flexbox 的盒子:

tsx
type Children = JSX.Element | JSX.Element[] | null
const Box = ({ children }: { children: Children }) => {
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      {children}
    </div>
  )
}

export default function App() {
  return (
    <Box>
      <h2>Hello!</h2>
      <h2>Hello!</h2>
    </Box>
  )
}
type Children = JSX.Element | JSX.Element[] | null
const Box = ({ children }: { children: Children }) => {
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      {children}
    </div>
  )
}

export default function App() {
  return (
    <Box>
      <h2>Hello!</h2>
      <h2>Hello!</h2>
    </Box>
  )
}

Event

tsx
function App() {
  return <div onClick={() => alert(1)}>
  	点我
  </div>
}
function App() {
  return <div onClick={() => alert(1)}>
  	点我
  </div>
}

ReactDOM ​

React 在 DOM(浏览器文本对象模型)下的渲染需要引入 react-dom。

typescript
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)

之所以要在这样设计,是因为 JSX 的结构是一个Virtual DOM,可以渲染在各个端:

  • React Native
  • Canavas
  • Web
  • ......

总结 ​

React 的核心设计原则是什么?

  • Learn once Write Anywhere;
  • 专注做好渲染工作;
  • Uniform:最简化、标准化的表达。

React Hooks API ​

useState ​

管理状态,当状态发生变化的时候反向通知 React 重绘。

通知方向:state hook => React Component => render

tsx
import { useState } from 'react'

export default function Aop() {
  // const [count, setCount] = useState(0)
  // const [count, setCount] = useState<number>(0)
  const [count, setCount] = useState(() => 0)

  return (
    <div>
      {count}
      {/* Race Condition */}
      <button onClick={() => setCount(x => x + 1)}>+</button>
    </div>
  )
}
import { useState } from 'react'

export default function Aop() {
  // const [count, setCount] = useState(0)
  // const [count, setCount] = useState<number>(0)
  const [count, setCount] = useState(() => 0)

  return (
    <div>
      {count}
      {/* Race Condition */}
      <button onClick={() => setCount(x => x + 1)}>+</button>
    </div>
  )
}

useEffect ​

当 React 渲染时,hook 中的函数根据依赖变化发生调用。

tsx
import { useEffect, useState } from 'react'
import { timer } from 'rxjs'

export default function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const subscription = timer(0, 1000).subscribe(() => setCount(x => x + 1))
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  useEffect(() => {
    console.log('count changed to', count)
  }, [count])

  return (
    <div>
      {count}
      <button onClick={() => setCount(x => x + 1)}>+</button>
    </div>
  )
}
import { useEffect, useState } from 'react'
import { timer } from 'rxjs'

export default function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const subscription = timer(0, 1000).subscribe(() => setCount(x => x + 1))
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  useEffect(() => {
    console.log('count changed to', count)
  }, [count])

  return (
    <div>
      {count}
      <button onClick={() => setCount(x => x + 1)}>+</button>
    </div>
  )
}

每当渲染的时候 useEffect 都会调用,React 通过判断依赖(deps)有无变化决定是否调用 useEffect 中的函数。这样就将函数调用和声明式的编程同意。

那么 react 可不可以这样写?

tsx
if (a === 1) {
  useEffect(() => {
    // ...
  }, [deps])
}
if (a === 1) {
  useEffect(() => {
    // ...
  }, [deps])
}

不能。

思考:能不能在 React 函数之外的地方使用 hooks?

不能。

思考:为什么叫做 effect?

去掉 useEffect,函数组件看起来很纯,像是靠 props 和 state 渲染,加上 effect 函数就不纯了,因为做了渲染之外的事情,比如设置定时器、打印日志、网络请求 ......

至于为什么叫做 Effect?我们通常将计算函数返回值之外的事情都称作 Effect。当这种 Effect 会产生负面效果,就称作 Side Effect。

useEffect 将渲染之外的事情收敛,集中管理。

useRef ​

这个 Hook 让函数组件可以在多次渲染间同步引用值。不过它为什么是钩子?谁触发它?其实就是每次渲染的时候触发这个 hook,然后它负责再保存一个引用。

tsx
function LogBottonClicks() {
  const countRef = useRef(0)

  const handle = () => {
    countRef.current++
    console.log(`Clicked ${countRef.current} times`)
  }

  console.log('rendered')

  return <button onClick={handle}>Click me</button>
}
function LogBottonClicks() {
  const countRef = useRef(0)

  const handle = () => {
    countRef.current++
    console.log(`Clicked ${countRef.current} times`)
  }

  console.log('rendered')

  return <button onClick={handle}>Click me</button>
}

利用这种机制,子组件可以向父组件同步数据:

tsx
function TextInputWithFocusButton() {
  const inputEl = useRef(null)

  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus()
  }

  return(
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  )
}
function TextInputWithFocusButton() {
  const inputEl = useRef(null)

  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus()
  }

  return(
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  )
}

思考:useRef 每次渲染的时候都调用,是如何做到只初始化一次的?

例如下面这个程序:

tsx

function LogButtonClicks() {
  const countRef = useRef(0)
  const [_, setVer] = useState(0)

  const handle = () => {
    countRef.current++
    console.log(`Clicked ${countRef.current} times`)
    setVer(ver => ver + 1)
  }

  return <button onClick={handle}>Click me</button>
}

function LogButtonClicks() {
  const countRef = useRef(0)
  const [_, setVer] = useState(0)

  const handle = () => {
    countRef.current++
    console.log(`Clicked ${countRef.current} times`)
    setVer(ver => ver + 1)
  }

  return <button onClick={handle}>Click me</button>
}

React 通过记录 useRef 的序号同步引用。比如 countRef 是函数组件的第 0 个 Ref,存放在位置 0。第一次渲染的时候,React 查看位置 0 中是否有值,如果没有初始化,就调用初始化函数/使用初始值。如果有,就不再初始化。

那么,我们这样使用可以的?答案是不可以。

tsx
let ref = null
if (a > 0) {
  ref = useRef(true)
} else {
  ref = useRef((false))
}
let ref = null
if (a > 0) {
  ref = useRef(true)
} else {
  ref = useRef((false))
}

hooks 本质也是一种对行为的描述,不可以在任何流程控制语句中使用。

总结 ​

请求数据逻辑放哪里?

useEffect 中。

React 父组件传递值给子组件如何做?子组件传递值给父组件如何做?

父传子:props。子传父:ref 或传递回调函数。

08. 类型计算 ​

TypeScript 提供了强大的类型计算能力。

typescript
type A1 = string | number // union
type B1 = string & number // never

type Point = { x: number; y: number }
type StateDesc = { state: string }

type X1 = Point | StateDesc
const ins01: X1 = { x: 1, y: 2 }
const ins02: X1 = { state: 'hello', y: 2 }

type X2 = Point & StateDesc
const ins03: X2 = { x: 1, y: 2, state: 'hello' }
// const ins04: X2 = { state: 'hello', y: 2 }
// 不能将类型“{ state: string; y: number; }”分配给类型“X2”。
// 类型 "{ state: string; y: number; }" 中缺少属性 "x",但类型 "Point" 中需要该属性。

interface A2 {
  foo(): void
}
interface A2 {
  bar(): void
}
type A1 = string | number // union
type B1 = string & number // never

type Point = { x: number; y: number }
type StateDesc = { state: string }

type X1 = Point | StateDesc
const ins01: X1 = { x: 1, y: 2 }
const ins02: X1 = { state: 'hello', y: 2 }

type X2 = Point & StateDesc
const ins03: X2 = { x: 1, y: 2, state: 'hello' }
// const ins04: X2 = { state: 'hello', y: 2 }
// 不能将类型“{ state: string; y: number; }”分配给类型“X2”。
// 类型 "{ state: string; y: number; }" 中缺少属性 "x",但类型 "Point" 中需要该属性。

interface A2 {
  foo(): void
}
interface A2 {
  bar(): void
}

使用复杂的 infer。

我们应该如何解决这个问题的描述?

typescript
function flattern(arr) {}
function flattern(arr) {}

首先我们可以这样描述:

typescript
function flattern(arr: Array<any>): Array<any> {}
function flattern(arr: Array<any>): Array<any> {}

但是这并不完整,还存在很多 any。那么这样呢?

typescript
type Flattened<T> = T extends V[] ? V : T
// 找不到名称“V”。
type Flattened<T> = T extends V[] ? V : T
// 找不到名称“V”。

这句已经非常接近我们的语义。ts 提供了一个关键字 infer,供我们推导类型。

typescript
type Flattened<T> = T extends Array<infer V> ? V : T
type D = Flattened<Array<number>> // number
type Flattened<T> = T extends Array<infer V> ? V : T
type D = Flattened<Array<number>> // number

我们还可以递归推导:

typescript
type Flattened<T> = T extends Array<infer V> ? Flattened<V> : T
type D = Flattened<Array<Array<number>>> // number
type Flattened<T> = T extends Array<infer V> ? Flattened<V> : T
type D = Flattened<Array<Array<number>>> // number

flattern 函数实现:

typescript
function flattern<T extends Array<any>>(arr: T): Array<Flattened<T>> {
  return new Array<Flattened<T>>().concat(...arr.map(x => (Array.isArray(x) ? flattern(x) : x)))
}

flattern([1, 2, 3, [3, 4, [5]]])
function flattern<T extends Array<any>>(arr: T): Array<Flattened<T>> {
  return new Array<Flattened<T>>().concat(...arr.map(x => (Array.isArray(x) ? flattern(x) : x)))
}

flattern([1, 2, 3, [3, 4, [5]]])

优化

typescript
type Atom = string | boolean | number | bigint
type Nested<T> = (T | (T | T[])[])[]

function flattern<T extends Atom>(arr: Nested<Atom>): Atom[] {
  return new Array<Atom>().concat(...arr.map(x => (Array.isArray(x) ? flattern(x) : x)))
}
type Atom = string | boolean | number | bigint
type Nested<T> = (T | (T | T[])[])[]

function flattern<T extends Atom>(arr: Nested<Atom>): Atom[] {
  return new Array<Atom>().concat(...arr.map(x => (Array.isArray(x) ? flattern(x) : x)))
}

或者这样实现也可以:

typescript
type Nested = Array<Atom | Nested>
const target: Nested = [1, 2, 3, [3, 4, [5]]]
type Nested = Array<Atom | Nested>
const target: Nested = [1, 2, 3, [3, 4, [5]]]

这其实就是一个递归的思考方式。

其他有意思的用法:

typescript
type Unwrapped<T> = T extends (infer U)[] ? U : T
type T0 = Unwrapped<Array<string>> // string
type Unwrapped<T> = T extends (infer U)[] ? U : T
type T0 = Unwrapped<Array<string>> // string
typescript
type Unwrapped<T> = T extends Promise<infer U> ? U : T
type T0 = Unwrapped<Promise<string>> // string
type Unwrapped<T> = T extends Promise<infer U> ? U : T
type T0 = Unwrapped<Promise<string>> // string

如果我们有一个 promise 数组,我们想把它拆成 string 的数组,应该如何做?

typescript
type Unwrapped<T> = T extends Array<infer U> ? (U extends Promise<infer R> ? R[] : U) : T
type Unwrapped<T> = T extends Array<infer U> ? (U extends Promise<infer R> ? R[] : U) : T

上面这种实现方式是可以的,不过这种写法整个类型体系不够模块化,那么我们应该如何去写?

typescript
type Unwrap<T> = T extends Promise<infer U>
  ? Unwrap<U>
  : T extends Array<infer V>
  ? UnwrapArray<T>
  : T
type UnwrapArray<T> = T extends Array<infer U> ? { [P in keyof T]: Unwrap<T[P]> } : T

type T0 = Unwrap<Promise<string>[]> // string[]
type T1 = Unwrap<Promise<Promise<number>>[]> // number[]
type Unwrap<T> = T extends Promise<infer U>
  ? Unwrap<U>
  : T extends Array<infer V>
  ? UnwrapArray<T>
  : T
type UnwrapArray<T> = T extends Array<infer U> ? { [P in keyof T]: Unwrap<T[P]> } : T

type T0 = Unwrap<Promise<string>[]> // string[]
type T1 = Unwrap<Promise<Promise<number>>[]> // number[]

09. Vue Latest ​

基本介绍 ​

Vue 是一个让然充满惊喜的框架,也是目前国内使用人数最多的前端框架。

在 vue 3 之后,写出的程序,很多时候已经和 react 看不出明显的区别。

例如下面这段程序:

tsx
type HelloWorldProps = {
	msg: string
}
export default ({ msg }: HelloWorldProps) => {
	return <h1>{ msg }</h1>
}

// babel/ts 支持 JSX
// JSX 并不意味着 react,JSX 是 HTTP 0.9 就设计出来的 HTML
// 程序语言本身扩展性是最好的
type HelloWorldProps = {
	msg: string
}
export default ({ msg }: HelloWorldProps) => {
	return <h1>{ msg }</h1>
}

// babel/ts 支持 JSX
// JSX 并不意味着 react,JSX 是 HTTP 0.9 就设计出来的 HTML
// 程序语言本身扩展性是最好的

react 和 vue 并不决定你的前端架构,它只是作为一个工具去支持你的渲染。MVVM 和 函数式都是前端框架中重要的组成部分。

特性解读 ​

vue 3,即 vue-next。

vue 3 可以说是对 vue 的程序应该如何写,重新下了定义:

  • JSX
  • Typescript
  • Composition API
  • reactivity

上面这几个更新并不全是 vue3 带来的,但是我们可以放到一起分析,算是对 vue-next 程序的定义。

why not sfc

可能有人会问?SFC 不香嘛?

首先我们来看一下,在 TS 环境下,SFC 需要一个 shim 文件:

tsx
declare module "*.vue" {
  import { DefineComponent } from 'vue'
  const Component: DefineComponent
  export default Component
}
declare module "*.vue" {
  import { DefineComponent } from 'vue'
  const Component: DefineComponent
  export default Component
}

declare 的作用:告诉 TypeScript 编译器 declare 的部分在源代码之外提供,不需要编译器处理。当遇到 *.vue 文件的时候,TS 编译时会将他们当作一个会 export default Component 的类型。

如果用 tsx 写,就不需要这个 shim。不过多一个 shim 少一个 shim 重要吗?

从架构角度来说很重要。通常你的项目概念越多,意味着设计越差。

在 SFC 中编写 template 和 script 标签,这个方式有两个缺点:

  • 不够灵活
    • 需要 v-show/v-if/v-for 等;
    • 关注点被分离(模板也好、script 也好,都是解决某个关注点的一部分,在 SFC 中被强行分离);
  • ts 类型检查
    • 函数组件可以在最大程度作用 TS 的类型检查(比如属性检查)

我们来看一个 vue 的计时器程序:

tsx
function useCounter(): [Ref<number>, () => void] {
  const counter = ref(0)
  
  function increment() {
  	counter.value++
  }
  
  return [counter, increment]
}

export default {
  setup() {
    const [counter, increment] = useCounter()
    return () => <div>
    	count is: {counter.value}
      <button onClick={increment}add></button>
    </div>
  }
}
function useCounter(): [Ref<number>, () => void] {
  const counter = ref(0)
  
  function increment() {
  	counter.value++
  }
  
  return [counter, increment]
}

export default {
  setup() {
    const [counter, increment] = useCounter()
    return () => <div>
    	count is: {counter.value}
      <button onClick={increment}add></button>
    </div>
  }
}

在这个程序中,我们可以看到:

  • counter 逻辑的集中管理;
  • 强大的封装能力;
  • 少量的记忆要求。

compostion api ​

composition api 是一系列函数式 API 的合集。

有用来初始化的、定义组件的:

  • setup
  • defineComponent

有支持响应式数据的:

  • ref
  • reactive
  • toRefs
  • computed
  • watch
  • watchEffect

有支持生命周期管理的:

  • onMounted
  • onUnmounted

总结来说,Composition API:

  • 提升组合能力(自定义的 Composition API)
  • 提供 Reactive Programming
  • 提供函数式(简化 API 设计)

vue 3.0 性能 ​

Vue3 并没有很很明显的提升性能,这个和 vue3 的渲染机制有关(没有时间切片)。

在一些大型前端系统,没有时间切片会导致页面打开执行周期过久。

Reactivity ​

R eactivity 是 Vue3 提供的核心能力,配合函数式的 Composition API 使用非常方便。

响应式编程 ​

Reactive Programming - 让类型自发的响应环境的变化。

**Reactive:**一个值是 Reactive,那么这个值可以被监听;一个对象是 Reactive,那么这个对象可以被监听。一个函数是 Reactive,那么这个函数在提供 Reactive 的能力,比如创造一个 Reactive 的值或者对象。

Be Reactive !!

让程序变的 Reactive 是很好的一个思路。程序如果不是 Reactive,那么往往是 Passive(被动的)。响应的的反义词为什么是被动?因为 Reactive 代表一部分程序(类型)主动的去通知周边自己做了什么,另一部分类型主动监听变化,主动做出判断并完成操作。

程序变的 Reactive 之后,每个模块好像就活了一样,不需要程序主动下命令,而是程序主动完成工作。从这个角度来看 Reactive 的反义词就是 Passive。

声明式(Declarative) ​

Reactive 的程序往往就是声明式的。所谓声明式,就是程序员的声明要做什么?不重要做什么?而不是写一大堆计算逻辑。

声明式需要更好的封装:

typescript
const arr = []
for (let i = 0; i < 1000; i++) {
	arr[i] = i  
}

// 声明式
const arr = range(0, 1000)
const arr = []
for (let i = 0; i < 1000; i++) {
	arr[i] = i  
}

// 声明式
const arr = range(0, 1000)

声明式会创造语言:

typescript
const div = document.createElement('div')
div.style.width = 100

// 声明式
const div = <div style={{ width: 100 }} />
const div = document.createElement('div')
div.style.width = 100

// 声明式
const div = <div style={{ width: 100 }} />

创造这个词并不够贴切。

声明式往往是 Reactive:

tsx
export default {
  setup() {
    const logined = ref(userStore.logined())
    
    useStore.on(e) {
      switch (e.type) {
        case 'logined':
          logined.value = true
          break
        case 'logout':
          logined.value = false
          break
      }
    }
    
    return () => {
      if (logined.value) {
        return <div>您好,欢迎光临</div>
      } else {
        return <div>请登录...</div>
      }
    }
  }
}
export default {
  setup() {
    const logined = ref(userStore.logined())
    
    useStore.on(e) {
      switch (e.type) {
        case 'logined':
          logined.value = true
          break
        case 'logout':
          logined.value = false
          break
      }
    }
    
    return () => {
      if (logined.value) {
        return <div>您好,欢迎光临</div>
      } else {
        return <div>请登录...</div>
      }
    }
  }
}

在上面的程序中组件在响应全局用户状态的变化,这样就将组件和全局状态解耦:

  • 组件迭代不用关心 useStore 迭代;
  • useStore 迭代,组件不需要调整。

其实上面代码还可以做优化,可以实现业务逻辑和组件的强分离:

tsx
function useLoginStatus() {
 	const logined = ref(userStore.logined())
    
  useStore.on(e) {
    switch (e.type) {
      case 'logined':
        logined.value = true
        break
      case 'logout':
        logined.value = false
        break
    }
  }
  
  return login.value
}

export default  {
  setup() {
    const loggedIn = useLoginStatus()
    return () => <LoginStatus loggedIn={loggedIn} />
  }
}

const LoginStatus = ({ loggedIn }) => {if
function useLoginStatus() {
 	const logined = ref(userStore.logined())
    
  useStore.on(e) {
    switch (e.type) {
      case 'logined':
        logined.value = true
        break
      case 'logout':
        logined.value = false
        break
    }
  }
  
  return login.value
}

export default  {
  setup() {
    const loggedIn = useLoginStatus()
    return () => <LoginStatus loggedIn={loggedIn} />
  }
}

const LoginStatus = ({ loggedIn }) => {if

还有一个最大的好处就是声明式可以帮助我们更好的阅读代码。

最后也是最重要的、上升到哲学高度的思考:Reactive 让程序模块自己懂得如何做事。

总结 ​

从前端看:如何理解 Reactivity?和 React 是一个词义吗?

从哲学角度去看,Reactivity 就是让程序知道如何自主做事,应该怎样去做。

个人认为 React 在设计之初就是期望程序可以 Reactivity,所以可以认为是一个词义。

组合能力是什么?Compostion API 提供更好的组合能力,这样说对吗?

组合能力其实就是最大能力的复用,即 “搭积木” 能力。这样肯定是对的。

Ref 和 Reactive ​

一个值如果是 Reactive,那么这个值应该:

  • 可以通知(trigger)
    • vue 更新
    • vue 做其他标准行为
    • 完成自定义行为
  • 可以监听(track)
    • vue 发生的变化(依赖)

ref(referece) ​

ref 是一个工厂方法,本质是创造一个 Ref 对象。ref 的目标是管理值。

首先,ref 可以像一个正常值一样被使用:

tsx
import { ref } from 'vue'

export default {
  setup() {
    const msg = ref('hello')
    return () => {
      return <div>{msg.value}</div>
    }
  }
}
import { ref } from 'vue'

export default {
  setup() {
    const msg = ref('hello')
    return () => {
      return <div>{msg.value}</div>
    }
  }
}

在上面的程序中:

  • setup 是一个 vue3 新特性(初始化组件);
  • setup 可以返回一个 render 函数,render 函数返回的 VNode。

jsx 的语法 div 会被 babel 翻译成 createVNode。

ref 是值的代理 ​

ref 是一个 setter 和 getter,例如下面这段代码就可以很好诠释 ref 的内部实现:

tsx
function createRef<T>(val: T) {
  let _val: T = val
  	
  const refObj = {
    set value(v: T) {
      console.log('setter called')
      _val = v
    },
    get value() {
      console.log('getter called')
      return _val
    }
  }
  return refObj
}

const a = createRef(0)
a.value = 10
a.value++ // 非原子操作,一次读写操作
console.log(a.value)
function createRef<T>(val: T) {
  let _val: T = val
  	
  const refObj = {
    set value(v: T) {
      console.log('setter called')
      _val = v
    },
    get value() {
      console.log('getter called')
      return _val
    }
  }
  return refObj
}

const a = createRef(0)
a.value = 10
a.value++ // 非原子操作,一次读写操作
console.log(a.value)

不过仅有 setter 和 getter 是不够的,还需要一些 reactive 的机制:

tsx
function trigger() {}
function track() {}

// ...

const refObj = {
  set value(v: T) {
    console.log('setter called')
    _val = v
    trigger()
  },
  get value() {
    console.log('getter called')
    track()
    return _val
  }
}
function trigger() {}
function track() {}

// ...

const refObj = {
  set value(v: T) {
    console.log('setter called')
    _val = v
    trigger()
  },
  get value() {
    console.log('getter called')
    track()
    return _val
  }
}

当 set 的时候 trigger,get 的时候 track。

  • trigger:驱动 vue 更新
  • track:跟踪 vue 更新
    • 一个 ref 可以给多个 vue 组件使用,因此依赖是不确定的;
    • 依赖(Deps)
      • vue 组件依赖 ref,因此是 ref 的依赖;
      • ref 的依赖应该是一个数组。
    • 为什么不能在 ref 的构造函数中确定依赖?
      • 构造函数和实际使用可能并不是一个位置,或同一个组件。
    • 为什么不自己操作依赖而是封装一个 track 方法?
      • 只有 get 的时候才需要操作依赖,实际时机很重要。
    • 为什么不是 vue 检查依赖,而是 ref track 更新依赖?
      • 发现能力更出色,更 Reactive。
ref 驱动更新示例 ​
tsx
import { ref } from 'vue'

export default {
  setup() {
    const counter = ref(0)
    return () => (
    	<div>
      	{counter.value}
        <button onClick={() => counter.value++}>add</button>
      </div>
    )
  }
}
import { ref } from 'vue'

export default {
  setup() {
    const counter = ref(0)
    return () => (
    	<div>
      	{counter.value}
        <button onClick={() => counter.value++}>add</button>
      </div>
    )
  }
}

Reactive ​

Reactive 和 Ref 类似,都是代理模式的 Reactive 值。

代理一个值用 getter 和 setter 很方便,代理一个对象,js 提供了 Proxy 类。

如何代理一个对象 ​
tsx
function createReactive(obj: any) {
  return new Proxy(obj, {
    get: (target, name, receiver) => {
      if (name ==== 'c') {
        return 'this is a proxy value'
      }
      return Reflect.get(target, name, receiver)
    },
    set: (target, name, value, receiver): boolean => {
      if (!(name in target)) return false
      Reflect.set(target, name, receiver)
      return true
    }
  })
}

const o = createReactive({
  a: 1,
  b: 2,
  foo: function() {
    console.log('a is', this.a)
  }
})

o.a = 100
console.log(o.c)

o.foo()
function createReactive(obj: any) {
  return new Proxy(obj, {
    get: (target, name, receiver) => {
      if (name ==== 'c') {
        return 'this is a proxy value'
      }
      return Reflect.get(target, name, receiver)
    },
    set: (target, name, value, receiver): boolean => {
      if (!(name in target)) return false
      Reflect.set(target, name, receiver)
      return true
    }
  })
}

const o = createReactive({
  a: 1,
  b: 2,
  foo: function() {
    console.log('a is', this.a)
  }
})

o.a = 100
console.log(o.c)

o.foo()

为什么用 Reflect.set、Reflect.get 而不用 target[name] 这种形式?

  • 可以在 getter 和 setter 间同步 receiver(this 指针)

比较坑的地方:

tsx
const p = new Proxy({
  a: 1
}, {
  get(target, property, receiver) {
    console.log('get trap', ...arguments)
    return Reflect.get(receiver, property, receiver)
  }
})
const p = new Proxy({
  a: 1
}, {
  get(target, property, receiver) {
    console.log('get trap', ...arguments)
    return Reflect.get(receiver, property, receiver)
  }
})

上面这段程序会造成栈溢出,因为 receiver 其实就是 proxy 代理对象,会造成循环读取。

Reactive 是一个代理对象 ​
tsx
const state = reactive({
  counter: 0
})

state.counter++
const state = reactive({
  counter: 0
})

state.counter++

上面的程序首先会触发代理对象的 getter,然后再触发 setter,因为 ++ 不是 atomic 原子操作。

具体实现和 ref 一致,Reactive 也会在 getter 中 track,在 setter 中 trigger。

Reactive 实现 Counter ​
tsx
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      counter: 0
    })
    
    return () => {
      <div>
      	{state.counter}
        <button
          onClick={() => {
            state.counter++
          }}
        >
          add
        </button>
      </div>
    }
  }
}
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      counter: 0
    })
    
    return () => {
      <div>
      	{state.counter}
        <button
          onClick={() => {
            state.counter++
          }}
        >
          add
        </button>
      </div>
    }
  }
}

Reactive vs Ref ​

它们都是 vue 提供的 reactive 值,Ref 维护一个值/对象,Reactive 维护一个对象的所有成员。

tsx
const obj = ref({
  a: 1,
  b: 2
})

obj.value.a++ // 不会触发重绘
obj.value = { ...obj.value, a: 1 } // 触发重绘

const obj1 = reactive({
  a: 1,
  b: 2
})
obj1.a++ // 触发重绘
const obj = ref({
  a: 1,
  b: 2
})

obj.value.a++ // 不会触发重绘
obj.value = { ...obj.value, a: 1 } // 触发重绘

const obj1 = reactive({
  a: 1,
  b: 2
})
obj1.a++ // 触发重绘

有一个函数叫做 toRef,还有一个函数叫做 toRefs,这两个函数可以将值转换为 ref。

tsx
import { defineComponent, reactive, toRef, toRefs } from 'vue'

export default defineComponent({
  setup() {
    const state = reactive({
      a: 1,
      b: '-'
    })
    
    const aRef = toRef(state, 'a')
    const bRef = toRef(state, 'b')
    // =>
    // const refs = toRefs(state)
    // const aRef === refs.a
    // cosnt bRef === refs.b
    
    return () => {
      return <>
      	<h1>aRef: { aRef.value }</h1>
        <h1>aRef: { aRef }</h1>
        <h1>bRef: { bRef.value }</h1>
        <h1>bRef: { bRef }</h1>
      	<button onClick={() => state.a++}>state.a++</button>
      	<button onClick={() => aRef.value++}>aRef.a++</button>
      </>
    }
  }
})
import { defineComponent, reactive, toRef, toRefs } from 'vue'

export default defineComponent({
  setup() {
    const state = reactive({
      a: 1,
      b: '-'
    })
    
    const aRef = toRef(state, 'a')
    const bRef = toRef(state, 'b')
    // =>
    // const refs = toRefs(state)
    // const aRef === refs.a
    // cosnt bRef === refs.b
    
    return () => {
      return <>
      	<h1>aRef: { aRef.value }</h1>
        <h1>aRef: { aRef }</h1>
        <h1>bRef: { bRef.value }</h1>
        <h1>bRef: { bRef }</h1>
      	<button onClick={() => state.a++}>state.a++</button>
      	<button onClick={() => aRef.value++}>aRef.a++</button>
      </>
    }
  }
})

Reactive 和 Ref 的类型 ​

在 Reactive 和 Ref 中,通过 UnwrapRef 的定义配合 infer 关键字,可以做到两种类型合一。

因此从类型的角度来看 ref 和 reactive 是同一类东西。

tsx
export declare function ref<T>(value: T): Ref<UnwrapRef<T>>
export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
 
export declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
 
// 拆包
export declare type UnwrapRef<T> = T extends Ref<infer V> ? UnwrapRefsSimple<V> : UnwrapRefSimple<T>
  
declare type UnwrapRefSimple<T> = 
	T extends
		Function |
		CollectionTypes |
		BaseTypes |
		Ref |
		RefUnwrapBailTypes[keyof RefUnwrapbBailTypes] ? 
			T :
			T extends Array<any> ?
				{
          [K in keyof T]: UnwraoRefSimple<T[K]>
        } : 
				T extends object ? UnwrappedObject<T>: T
export declare function ref<T>(value: T): Ref<UnwrapRef<T>>
export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
 
export declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
 
// 拆包
export declare type UnwrapRef<T> = T extends Ref<infer V> ? UnwrapRefsSimple<V> : UnwrapRefSimple<T>
  
declare type UnwrapRefSimple<T> = 
	T extends
		Function |
		CollectionTypes |
		BaseTypes |
		Ref |
		RefUnwrapBailTypes[keyof RefUnwrapbBailTypes] ? 
			T :
			T extends Array<any> ?
				{
          [K in keyof T]: UnwraoRefSimple<T[K]>
        } : 
				T extends object ? UnwrappedObject<T>: T

总结 ​

vue 3 提供的 Reactive 这种编程模式和之前 vue.observable 有什么区别?

其实是整体编程风格的转变。

tsx
const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: {
        click: () => { state.count++ }
      },
      `count is: ${ state.count }`
    })
  }
}
const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: {
        click: () => { state.count++ }
      },
      `count is: ${ state.count }`
    })
  }
}

什么是 observable?reactive is observable?

  • 可以被观察到的(observable);
    • rxjs 中一个 observable 同时依然是 observer。
  • 可以观察别人(observer)。

10. 函数补充部分 ​

构造函数表达 ​

typescript
type SomeConstructor = {
  new (s: string): String
}

function fn(ctor: SomeConstructor) {
  return new ctor('hello')
}

const str = fn(String)
console.log(str) // hello
type SomeConstructor = {
  new (s: string): String
}

function fn(ctor: SomeConstructor) {
  return new ctor('hello')
}

const str = fn(String)
console.log(str) // hello
typescript
type SomeConstructor<T> = {
  new (s: number): T
}

function fn<T>(ctor: SomeConstructor<T>, n: number) {
  return new ctor(n)
}

fn<Array<string>>(Array, 100)
type SomeConstructor<T> = {
  new (s: number): T
}

function fn<T>(ctor: SomeConstructor<T>, n: number) {
  return new ctor(n)
}

fn<Array<string>>(Array, 100)

泛型函数 ​

typescript
function firstElement<Type>(arr: Type[]): Type {
  return arr[0]
}
function firstElement<Type>(arr: Type[]): Type {
  return arr[0]
}

关于推导 ​

typescript
// map: a => b
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func)
}

const parsed = map(['1', '2', '3'], n => parseInt(n))
// [1, 2, 3]
// map: a => b
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func)
}

const parsed = map(['1', '2', '3'], n => parseInt(n))
// [1, 2, 3]

泛型约束 ​

巨坑:

typescript
function minimunLength<Type extends { length: number }>(obj: Type, minimum: number): Type {
  if (obj.length >= minimum) return obj
  return { length: minimum }
  // 不能将类型“{ length: number; }”分配给类型“Type”。
  // "{ length: number; }" 可赋给 "Type" 类型的约束,但可以使用约束 "{ length: number; }" 的其他子类型实例化 "Type"。ts(2322)
}
function minimunLength<Type extends { length: number }>(obj: Type, minimum: number): Type {
  if (obj.length >= minimum) return obj
  return { length: minimum }
  // 不能将类型“{ length: number; }”分配给类型“Type”。
  // "{ length: number; }" 可赋给 "Type" 类型的约束,但可以使用约束 "{ length: number; }" 的其他子类型实例化 "Type"。ts(2322)
}

原因:

  • 泛型约束约束的是 Type 必须具有 length 属性;
  • 有 length 属性,不代表一定是目标 Type。
typescript
function minimunLength<Type extends { length: number }>(obj: Type, minimum: number): Type {
  if (obj.length >= minimum) return obj
  return obj.constructor(minimum)
}
function minimunLength<Type extends { length: number }>(obj: Type, minimum: number): Type {
  if (obj.length >= minimum) return obj
  return obj.constructor(minimum)
}

我们可以使用 constructor 解决上述问题。

typescript
function minimunLength<Type extends { length: number; constructor: Function }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) return obj
  return obj.constructor(minimum)
}
function minimunLength<Type extends { length: number; constructor: Function }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) return obj
  return obj.constructor(minimum)
}

手动指定类型 ​

typescript
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2)
}

const arr = combine([1, 2, 3], ['hello'])
// 不能将类型“string”分配给类型“number”。
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2)
}

const arr = combine([1, 2, 3], ['hello'])
// 不能将类型“string”分配给类型“number”。

这种时候可以手动指定类型:

typescript
const arr = combine<string | number>([1, 2, 3], ['hello'])
const arr = combine<string | number>([1, 2, 3], ['hello'])

使用泛型的一些规范 ​

对比下面这一组,哪种比较好?

typescript
function firstElement1<Type>(arr: Type[]) {
  return arr[0]
}
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0]
}
function firstElement1<Type>(arr: Type[]) {
  return arr[0]
}
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0]
}

利用好推导(infer)能力,避免使用 any。

对比下面这一组,哪种比较好?

typescript
function filter1<Type>(arr: Type[], func: (args: Type) => boolean): Type[] {
  return arr.filter(func)
}
function filter2<Type, Func extends (args: Type) => boolean>(arr: Type[], func: Func): Type[] {
  return arr.filter(func)
}
function filter1<Type>(arr: Type[], func: (args: Type) => boolean): Type[] {
  return arr.filter(func)
}
function filter2<Type, Func extends (args: Type) => boolean>(arr: Type[], func: Func): Type[] {
  return arr.filter(func)
}

减少泛型参数的使用,除非有必要。

下面这个程序好嘛?

typescript
function greet<Str extends string>(s: Str) {
  console.log('Hello, ' + s)
}
function greet<Str extends string>(s: Str) {
  console.log('Hello, ' + s)
}

不好,s 本来就是 string 类型。

可选参数 ​

用 ? 描述函数的可选参数。

typescript
function myForEach(arr: any[], callback: (args: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i)
  }
}
function myForEach(arr: any[], callback: (args: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i)
  }
}

函数重载 ​

typescript
function add<T>(a: T, b: T) {
  return a + b
  // 运算符“+”不能应用于类型“T”和“T”。
}
function add<T>(a: T, b: T) {
  return a + b
  // 运算符“+”不能应用于类型“T”和“T”。
}

修改办法:

typescript
function isSet<T>(x: any): x is Set<T> {
  return x instanceof Set
}

function add(a: number, b: number): number
function add(a: string, b: string): string
function add<T>(a: Set<T>, b: Set<T>): Set<T>
function add<T>(a: T, b: T): T {
  if (isSet<T>(a) && isSet<T>(b)) {
    return new Set([...a, ...b]) as any
  }
  return (a as any) + (b as any)
}

const a = new Set<string>(['apple', 'redhat'])
const b = new Set<string>(['google', 'ms'])

console.log(add(a, b))
console.log(add(1, 2))
console.log(add('a', 'k'))
function isSet<T>(x: any): x is Set<T> {
  return x instanceof Set
}

function add(a: number, b: number): number
function add(a: string, b: string): string
function add<T>(a: Set<T>, b: Set<T>): Set<T>
function add<T>(a: T, b: T): T {
  if (isSet<T>(a) && isSet<T>(b)) {
    return new Set([...a, ...b]) as any
  }
  return (a as any) + (b as any)
}

const a = new Set<string>(['apple', 'redhat'])
const b = new Set<string>(['google', 'ms'])

console.log(add(a, b))
console.log(add(1, 2))
console.log(add('a', 'k'))

利用重载约束跨类型方法的使用。

操作符重载 ​

参考资料:babel-plugin-overload-operator

Typescript 并不支持操作符重载,我们可以借助 babel 插件来实现。

THIS ​

typescript
interface DB {
  exec(sql: string): any
}
function runSql(this: DB, sql: string) {
  this.exec(sql)
}

runSql('select * from user')
interface DB {
  exec(sql: string): any
}
function runSql(this: DB, sql: string) {
  this.exec(sql)
}

runSql('select * from user')

this 和 new 都是 typescript 提供的关键字。

void vs unknown ​

函数不返回参数用 void,返回的值类型不确定用 unkown。

下面的定义会报错吗?

typescript
type voidFunc = () => void

const f1: voidFunc = () => {
  return true
}
const f2: voidFunc = () => true
const f3: voidFunc = function () {
  return true
}
type voidFunc = () => void

const f1: voidFunc = () => {
  return true
}
const f2: voidFunc = () => true
const f3: voidFunc = function () {
  return true
}

上面这三条都不会报错。但是类型推导的返回值并不能使用。

unknown 可以让代码更安全。

typescript
function f1(a: any) {
  a.b() // pass
}
function f2(a: unknown) {
  a.b()
  // 类型“unknown”上不存在属性“b”。
}
function f1(a: any) {
  a.b() // pass
}
function f2(a: unknown) {
  a.b()
  // 类型“unknown”上不存在属性“b”。
}

为什么会写下面这样的程序:

typescript
function safeParse(s: string): unknown {
  return JSON.parse(s)
}
function fail(msg: string) {
  throw new Error(msg)
}
function safeParse(s: string): unknown {
  return JSON.parse(s)
}
function fail(msg: string) {
  throw new Error(msg)
}

rest params ​

typescript
function multiply(n: number, ...m: number[]) {
  return m.map(x => n * x)
}

const a = multiply(10, 1, 2, 3, 4)
function multiply(n: number, ...m: number[]) {
  return m.map(x => n * x)
}

const a = multiply(10, 1, 2, 3, 4)

总结 ​

泛型帮助我们让类型检查更加严格,更加智能。

泛型不是让写程序更舒服的一种方式,而是让你的程序更加可预测。

11. 类型计算 ​

Keyof 操作符 ​

typescript
type Point = { x: number; y: number }
type P = keyof Point // "x" | "y"

type Arrayish = { [n: number]: unknown }
type A = keyof Arrayish // number

type Mapish = { [k: string]: boolean }
type M = keyof Mapish // string | number
type Point = { x: number; y: number }
type P = keyof Point // "x" | "y"

type Arrayish = { [n: number]: unknown }
type A = keyof Arrayish // number

type Mapish = { [k: string]: boolean }
type M = keyof Mapish // string | number

M 类型是 string | number ,因为对于对象来说,虽然规定 k 是 string,但是也可以使用数字作为键。

typeof ​

typescript
console.log(typeof 'xxx') // string

let s = 'hello'
let n: typeof s // string
console.log(typeof 'xxx') // string

let s = 'hello'
let n: typeof s // string

typeof 在 javascript 是计算操作符,不过在 ts 中,属于类型操作符(不是运行时执行,coding 时也会检查)。

Partial Type ​

部分类型,ts 的延伸能力。

typescript
interface Todo {
  title: string
  description: string
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return {
    ...todo,
    ...fieldsToUpdate
  }
}

const todo1 = {
  title: 'organize desk',
  description: 'clear clutter'
}
const todo2 = updateTodo(todo1, {
  description: 'throw out trash'
})
interface Todo {
  title: string
  description: string
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return {
    ...todo,
    ...fieldsToUpdate
  }
}

const todo1 = {
  title: 'organize desk',
  description: 'clear clutter'
}
const todo2 = updateTodo(todo1, {
  description: 'throw out trash'
})

如何实现?

typescript
type Partial<T> = {
  [P in keyof T]?: T[P]
}
type Partial<T> = {
  [P in keyof T]?: T[P]
}

Required ​

typescript
interface Props {
  a?: number
  b?: number
}

const obj: Props = { a: 5 }
const obj2: Required<Props> = { a: 5 }
// 类型 "{ a: number; }" 中缺少属性 "b",但类型 "Required<Props>" 中需要该属性。
interface Props {
  a?: number
  b?: number
}

const obj: Props = { a: 5 }
const obj2: Required<Props> = { a: 5 }
// 类型 "{ a: number; }" 中缺少属性 "b",但类型 "Required<Props>" 中需要该属性。

如何实现?

typescript
type Required<T> = {
  [P in keyof T]-?: T[P]
}
type Required<T> = {
  [P in keyof T]-?: T[P]
}

Readonly ​

typescript
interface Todo {
  title: string
}

const todo: Readonly<Todo> = {
  title: 'Delete inactive users'
}

todo.title = 'Hello'
// 无法为“title”赋值,因为它是只读属性。
interface Todo {
  title: string
}

const todo: Readonly<Todo> = {
  title: 'Delete inactive users'
}

todo.title = 'Hello'
// 无法为“title”赋值,因为它是只读属性。

如何实现?

typescript
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

Record ​

记录。描述 key value 值的类型。

typescript
interface CatInfo {
  age: number
  breed: string
}

type CateName = 'miffy' | 'boris' | 'mordred'

const cats: Record<CateName, CatInfo> = {
  miffy: { age: 10, breed: 'persian' },
  boris: { age: 5, breed: 'Maine Coon' },
  mordred: { age: 10, breed: 'Britsh Shorthair' }
}
interface CatInfo {
  age: number
  breed: string
}

type CateName = 'miffy' | 'boris' | 'mordred'

const cats: Record<CateName, CatInfo> = {
  miffy: { age: 10, breed: 'persian' },
  boris: { age: 5, breed: 'Maine Coon' },
  mordred: { age: 10, breed: 'Britsh Shorthair' }
}

如何实现?

typescript
type Record<K extends keyof any, T> = {
  [P in K]: T
}
type Record<K extends keyof any, T> = {
  [P in K]: T
}

日常使用:

typescript
const obj = Record<string, object>
const obj = Record<string, object>

Pick ​

typescript
interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false
}
interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false
}

如何实现?

typescript
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

Exclude ​

typescript
type T0 = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'> // 'c'
type T2 = Exclude<string | number | (() => void), Function> // string | number
type T0 = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'> // 'c'
type T2 = Exclude<string | number | (() => void), Function> // string | number

如何实现?

typescript
type Exclude<T, U> = T extends U ? never : T
type Exclude<T, U> = T extends U ? never : T

Omit ​

省略。与 Pick 是相反的操作。

Exclude 操作的是联合类型,Omit 操作的是接口。

typescript
interface Todo {
  title: string
  description: string
  completed: boolean
  createdAt: number
}

type TodoPreview = Omit<Todo, 'description'>
const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
  createdAt: 1681344482208
}

type TodoInfo = Omit<Todo, 'completed' | 'createdAt'>
const todoInfo: TodoInfo = {
  title: 'Pick up kids',
  description: 'Kindergarten closes at 5pm'
}
interface Todo {
  title: string
  description: string
  completed: boolean
  createdAt: number
}

type TodoPreview = Omit<Todo, 'description'>
const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
  createdAt: 1681344482208
}

type TodoInfo = Omit<Todo, 'completed' | 'createdAt'>
const todoInfo: TodoInfo = {
  title: 'Pick up kids',
  description: 'Kindergarten closes at 5pm'
}

如何实现:

typescript
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

Extract ​

typescript
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'> // 'a'
type T1 = Extract<string | number | (() => void), Function> // () => void
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'> // 'a'
type T1 = Extract<string | number | (() => void), Function> // () => void

如何实现?

typescript
type Extract<T, U> = T extends U ? T : never
type Extract<T, U> = T extends U ? T : never

可以看到,Extract 和 Exclude 是一对组合;Pick 和 Omit 是一对组合。

NonNullable ​

typescript
type T0 = NonNullable<string | number | undefined> // string | number
type T1 = NonNullable<string[] | null | undefined> // string[]
type T0 = NonNullable<string | number | undefined> // string | number
type T1 = NonNullable<string[] | null | undefined> // string[]

如何实现:

typescript
 type NonNullable<T> = T extends null | undefined ? never : T
 type NonNullable<T> = T extends null | undefined ? never : T

Parmameters ​

typescript
declare function f1(args: { a: number; b: string }) : void

type T0 = Parameters<() => string> // []
type T1 = Parameters<(s: string) => void> // [s: string]
type T2 = Parameters<<T>(args: T) => T> // [args: unknown]
type T3 = Parameters<typeof f1> // [args: { a:number; b: string }]
declare function f1(args: { a: number; b: string }) : void

type T0 = Parameters<() => string> // []
type T1 = Parameters<(s: string) => void> // [s: string]
type T2 = Parameters<<T>(args: T) => T> // [args: unknown]
type T3 = Parameters<typeof f1> // [args: { a:number; b: string }]

如何实现:

typescript
type Parameters<T extends (...args: any) => any> = 
	T extends (...args: infer P) => any ? P : never
type Parameters<T extends (...args: any) => any> = 
	T extends (...args: infer P) => any ? P : never

ConstructorParameters ​

typescript
type T0 = ConstructorParameters<ErrorConstructor> // [message?: string | undefiend]
type T1 = ConstructorParameters<FunctionConstructor> // string[]
type T2 = ConstructorParameters<RegExpConstructor> // [pattern: string | RegExp, flags?: string | undefined]
type T3 = ConstructorParameters<any> // unkown[]

type T4 = ConstructorParameters<Function>
// 类型“Function”不满足约束“abstract new (...args: any) => any”。
// 类型“Function”提供的内容与签名“new (...args: any): any”不匹配。
type T0 = ConstructorParameters<ErrorConstructor> // [message?: string | undefiend]
type T1 = ConstructorParameters<FunctionConstructor> // string[]
type T2 = ConstructorParameters<RegExpConstructor> // [pattern: string | RegExp, flags?: string | undefined]
type T3 = ConstructorParameters<any> // unkown[]

type T4 = ConstructorParameters<Function>
// 类型“Function”不满足约束“abstract new (...args: any) => any”。
// 类型“Function”提供的内容与签名“new (...args: any): any”不匹配。

如何实现:

typescript
type ConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never
type ConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never

ReturnType ​

typescript
declare function f1(): { a: number; b: string }

type T0 = ReturnType<() => string> // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T> // unkown
type T3 = ReturnType<<T extends U, U extends number[]>() => T> // number[]
type T4 = ReturnType<typeof f1> // { a: number; b: string }
type T5 = ReturnType<any> // any
type T6 = ReturnType<never> // never
type T7 = ReturnType<string> // 类型“string”不满足约束“(...args: any) => any”。
type T8 = ReturnType<Function> // 类型“Function”不满足约束“(...args: any) => any”。
declare function f1(): { a: number; b: string }

type T0 = ReturnType<() => string> // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T> // unkown
type T3 = ReturnType<<T extends U, U extends number[]>() => T> // number[]
type T4 = ReturnType<typeof f1> // { a: number; b: string }
type T5 = ReturnType<any> // any
type T6 = ReturnType<never> // never
type T7 = ReturnType<string> // 类型“string”不满足约束“(...args: any) => any”。
type T8 = ReturnType<Function> // 类型“Function”不满足约束“(...args: any) => any”。

如何实现:

typescript
type ReturnType<T extends (...args: any) => any> = 
	T extends (...args: any) => infer R ? R : any
type ReturnType<T extends (...args: any) => any> = 
	T extends (...args: any) => infer R ? R : any

InstanceType ​

typescript
class C {
  x = 0
  y = 0
}

type T0 = InstanceType<typeof C> // C
type T1 = InstanceType<any> // any
type T2 = InstanceType<never> // never
type T3 = InstanceType<string> // 类型“string”不满足约束“abstract new (...args: any) => any”。
type T4 = InstanceType<Function> // 类型“Function”不满足约束“abstract new (...args: any) => any”。
class C {
  x = 0
  y = 0
}

type T0 = InstanceType<typeof C> // C
type T1 = InstanceType<any> // any
type T2 = InstanceType<never> // never
type T3 = InstanceType<string> // 类型“string”不满足约束“abstract new (...args: any) => any”。
type T4 = InstanceType<Function> // 类型“Function”不满足约束“abstract new (...args: any) => any”。

如何实现?

typescript
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (
  ...args: any
) => infer R
  ? R
  : any
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (
  ...args: any
) => infer R
  ? R
  : any

ThisParameterType ​

typescript
function toHex(this: Number) {
  return this.toString(16)
}

function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n)
}
function toHex(this: Number) {
  return this.toString(16)
}

function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n)
}

如何实现?

typescript
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown

OmitThisParameter ​

typescript
function toHex(this: Number) {
  return this.toString(16)
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5)
console.log(fiveToHex())
function toHex(this: Number) {
  return this.toString(16)
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5)
console.log(fiveToHex())

如何实现?

typescript
type OmitThisParameter<T> = unknown extends ThisParameterType<T>
  ? T
  : T extends (...args: infer A) => infer R
  ? (...args: A) => R
  : T
type OmitThisParameter<T> = unknown extends ThisParameterType<T>
  ? T
  : T extends (...args: infer A) => infer R
  ? (...args: A) => R
  : T

ThisType ​

提供 this 提示。

typescript
type ObjectDescriptor<D, M> = {
  data?: D
  methods: M & ThisType<D & M>
}

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {}
  let methods: object = desc.methods || {}
  return {
    ...data,
    ...methods
  } as D & M
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx
      this.y += dy
    }
  }
})

obj.x = 10
obj.y = 20
obj.moveBy(5, 5)
type ObjectDescriptor<D, M> = {
  data?: D
  methods: M & ThisType<D & M>
}

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {}
  let methods: object = desc.methods || {}
  return {
    ...data,
    ...methods
  } as D & M
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx
      this.y += dy
    }
  }
})

obj.x = 10
obj.y = 20
obj.moveBy(5, 5)
json
// tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "lib": ["es6"],
    "esModuleInterop": true,
    "noImplicitThis": true, // 需要打开这个配置
    "downlevelIteration": true,
    "module": "CommonJS"
  }
}
// tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "lib": ["es6"],
    "esModuleInterop": true,
    "noImplicitThis": true, // 需要打开这个配置
    "downlevelIteration": true,
    "module": "CommonJS"
  }
}

如何实现?

typescript
interface ThisType<T> {}
interface ThisType<T> {}

Uppercase/LowerCase ​

typescript
type Greeting = 'Hello, world'
type ShoutGreeting = Uppercase<Greeting> // HELLO, WORLD

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<'my_app'> // ID-MY_APP

type QuietGreeting = Lowercase<ShoutGreeting> // hello, world
type Greeting = 'Hello, world'
type ShoutGreeting = Uppercase<Greeting> // HELLO, WORLD

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<'my_app'> // ID-MY_APP

type QuietGreeting = Lowercase<ShoutGreeting> // hello, world

如何实现?

typescript
type Uppercase<S extends string> = intrinsic
type Lowercase<S extends string> = intrinsic
type Capitalize<S extends string> = intrinsic
type Uncapitalize<S extends string> = intrinsic
type Uppercase<S extends string> = intrinsic
type Lowercase<S extends string> = intrinsic
type Capitalize<S extends string> = intrinsic
type Uncapitalize<S extends string> = intrinsic

intrinsic 代表这个实现是内部实现,不是 ts 直接可以使用。

总结 ​

类型是可以计算的吗?答案当然是肯定的。

  • &
  • ?
  • infer
  • ......