Flow 和 TypeScript 基础回顾
类型系统基础概念
强类型与弱类型
从类型安全角度区分类型。
- 强类型从语言层面限制函数的实参类型必须与形参相同
- 弱类型在语言层面不会限制实参的类型
- 强类型语言中不允许有任意的隐式类型转换
- 弱类型语言则允许任务的数据隐式类型转换
- 变量类型允许随时改变的特点,不是强弱类型的差异
静态类型与动态类型
从类型检查角度区分
- 静态类型:一个变量声明时它的类型就是明确的,声明过后,类型就不能修改
- 动态类型:运行阶段才能够明确变量类型,而且变量的类型随时可以改变
JavaScript 自有类型系统问题
JavaScript 既是弱类型又是动态类型,非常*任性,没有任何限制
JavaScript 是脚本语言,没有编译环节直接执行,也就没有编译环节去检查类型,而静态语言要先经过编译再执行。
而在现如今 JavaScript 大规模使用,就会暴露一些弱类型,动态类型的问题
- 异常需要等到运行时才能发现
let obj = {}
obj.handle() // obj中没有该属性方法,只有当代码运行才会报出异常
- 函数功能可能发生改变
function sum (a, b) {
return a + b
}
console.log(sum(100, 100))
console.log(sum(100, '100')) // 此时变成了字符串拼接
- 对象索引器的错误用法
const obj = {}
obj[true] = 100 // 属性名会自动转换为字符串
console.log(obj['true'])
相比于弱类型语言,强类型语言有以下的优势:
- 强类型代码错误更早暴露
- 强类型代码更智能,编码更准确( 编辑器可以更准确的智能提示具体类型 )
- 重构更方便 (比如,在修改某些复用对象的属性名时,可以更清晰的提示所有需要修改的位置)
- 减少不必要的类型判断
Flow 静态类型检查方案
Flow 是 JavaScript 的静态类型检查器,Flow 发布在 GitHub 上. 在 Facebook 内部大量使用,并且是开源的.
Flow 使用类型注解,来给参数添加类型控制。在生产环境中可以通过编译工具,去掉类型注解,我们可以在自己需要的地方添加类型注解
Flow 快速上手
在项目中使用flow
- 安装flow-bin
npm i flow-bin -D
- 在要被检查的文件顶部添加
// @flow
标记文件要被flow进行检查 - 在package.json中的script中添加命令
"flow": "flow"
- 创建flowconfig文件,运行
npm run flow init
- 运行命令
npm run flow
移除类型注解
-
方案一:自动移除类型注解,官方提供的模块:
- 运行
npm i flow-remove-types --dev
安装flow-remove-types模块 - 在package.json的script 中添加命令
"flow-remove-types":"flow-remove-types . -d dist"
- 运行
npm run flow-remove-types
生成dist目录,其中的文件就是编译后的。
- 运行
-
方案二:babel
-
安装babel
npm i @babel/core @babel/cli @babel/preset-flow -D
-
创建.babelrc文件
{ "presets": [ "flow" ] }
-
在package.json的script 中添加
"build":"babel . -d dist"
-
运行
npm run build
-
安装VSCode插件来实时监听类型错误,但是需要保存之后才能看到错误的波浪线
Flow 基本使用
- flow 可以为我们的代码,进行类型推断,如下图,函数参数没有添加注解但是flow可以根据运算符推断参数类型,并且给出错误提示,但是还是建议给参数都加上类型注解
-
类型注解,类型注解不仅可以给函数参数标记类型还可以用来标记变量的类型和函数返回值的类型
/** * 类型注解 * * @flow */ function square (n: number) { return n * n } let num: number = 100 // num = 'string' // error function foo (): number { // 标记函数的返回值是number return 100 // ok // return 'string' // error } function bar (): void { // 标记函数没有返回值 // return undefined }
- 原始类型 目前原始类型共有6种,number、boolean、string、null、undefined、symbol,这里主要的是 undefined 是需要用void进行表示
-
结构类型
- 数组类型
/** * 数组类型 * * @flow */ const arr1: Array<number> = [1, 2, 3] // 写法一 const arr2: number[] = [1, 2, 3] // 写法二 // 元组 const foo: [string, number] = ['foo', 100] // 数据结构需要跟左侧结构一致
- 对象类型
/** * 对象类型 * * @flow */ const obj1: { foo: string, bar: number } = { foo: 'string', bar: 100 } const obj2: { foo?: string, bar: number } = { bar: 100 } // ? 表示可选 const obj3: { [string]: string } = {} // 表示允许任意个数的键 但是必须要是string obj3.key1 = 'value1' obj3.key2 = 'value2'
- 函数类型
/** * 函数类型 * * @flow */ // 限制foo函数只能接收一个回调函数作为参数 // 并且这个回调函数的参数必须是string 和 number, 没有返回值 function foo (callback: (string, number) => void) { callback('string', 100) } foo(function (str, n) { // str => string // n => number })
-
特殊类型
- 字面量类型
const a: 'foo' = 'foo'
a的值只能是foo字符串 - 或类型
const type: 'success' | 'warning' | 'danger' = 'success'
type的值只能是三者之一 - 声明类型, 通过type声明一个类型别名
type StringOrNumber = string | number const b: StringOrNumber = 'string' // 100
- Maybe类型
// gender的值 也许是数字也可以是undefined const gender: ?number = undefined
- 字面量类型
-
Mixed与any
两者都表示任意类型,但是Mixed 还是强类型,需要添加 typeof 判断类型
而any 就是随便使用类似于js原始的类型特性function passMixed (value: mixed) { if (typeof value === 'string') { value.substr(1) } if (typeof value === 'number') { value * value } }
function passAny (value: any) { value.substr(1) value * value }
TypeScript 语言规范与基本应用
- TypeScript可以编译出纯净、 简洁的JavaScript代码,并且可以运行在任何浏览器上、Node.js环境中和任何支持ECMAScript
3(或更高版本)的JavaScript引擎中- 类型允许JavaScript开发者在开发JavaScript应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构
- TypeScript提供最新的和不断发展的JavaScript特性,包括那些来自2015年的ECMAScript和未来的提案中的特性
以上这些是ts的优势,但是ts也存在缺陷:
- 语言本身多了很多概念,提高了开发人员学习成本
- 项目初期,ts会需要编程很多类型声明,增加开发成本
基本使用
ts 可以安装到全局,也可以在项目中安装
全局安装
- 首先运行
npm install -g typescript
全局安装ts - 然后编写 .ts 后缀名的文件
- 在终端运行
tsc 文件名.ts
就可以自动生成编译好的js文件
局部安装
使用yarn
- yarn init --yes 创建项目
- yarn add typescript --dev 安装 TypeScript 模块
- yarn tsc 文件名.ts 运行编译
- 推荐使用yarn 运行tsc指令不需要修改package.json
使用npm
- 在项目目录运行
npm i typescript -D
- 在package.json文件的script中添加命令设置
“tsc”:"tsc"
- 编写.ts后缀名的文件 在终端运行
npm run tsc 文件名.ts
ts配置文件
运行 yarn tsc --init
创建tsconfig.json 配置文件。
我们来了解几个简单的配置项含义
- “target”: “ES5” ts编译成ECMAScript对应版本的js,
- “module”: “commonjs” ts编译模块化导入的标准
- “outDir”: “dist” 编译好的js文件输出到dist目录
- “rootDir”:“src” 要编译的文件夹,会把文件夹内的ts文件都进行编译
- “sourceMap”: true 开启源代码映射方便调试
- “strict”: true 严格检查模式
原始数据类型
ts 中的原始数据类型与flow类似 ,但是根据strict设置不同,也有一些差异。
string 在严格模式下 不能为null或者undefined
boolean 在严格模式下 不能为null或者undefined
number 在严格模式下 不能为null或者undefined
null
undefined
symbol 使用symbol要注意把target设置为ES2015, 因为ES2015之前是没有symbol类型的
void 表示一个函数没有返回值,严格模式下默认值是undefined
这里我们需要解决一个标准库的问题,因为如果在编码中使用了非当前标准库的语法那么TS就会报错,我们可以通过修改ts配置文件中的lib 属性来添加标准库
// 如果只设置ES2015就会覆盖掉默认标准库导致其他环境api报错,所以还需要引入不同环境下的标准库比如DOM
// typeScript中把BOM和DOM归结到一个标准库DOM当中了,所以只需要添加一个DOM
"lib": ["ES2015","DOM"]
标准库就是内置对象所对应的声明,我们在代码中使用内置对象就要使用对应的标准库,否则typeScript就找不到对应的类型,就会报错
不同文件的变量重名问题
在同一目录下的不同ts文件中,如果声明的变量重名了,会报错重名。这是作用域的问题,因为这些变量都是声明在全局上的,所以会提示错误。
- 方案一, 使用自调用函数提供封闭作用域
- 方案二, 在代码中提供 export {} 将其中所有成员作为模块成员
其他数据类型
object 类型
object 类型,并不是限制只能传入对象,而是指所有非原始数据类型之外的类型,可以是函数也可以使数组也可以是对象
// Object 类型
export {} // 确保跟其它示例没有成员冲突
// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {}
// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
数组类型
- 可以写成
const arr1: Array<number> = [2,3,4]
- 可以写成
const arr2: number[] = [1, 2, 3]
假设我们定义一个函数 接收不固定数量的参数,要求这些参数都是数字类型。如果是在js中我们可能需要添加参数类型的判断,而如果使用ts 就会简单很多
function fn(...args: number[]){ ……… }
元祖类型
元组就是明确元素数量以及每个元素类型的数组
// 元组就是明确元素数量以及每个元素类型的数组
// 限制tuple的值只能是长度为2 第一个成员类型为数字,第二个成员类型为字符串的结构
const tuple: [number, string] = [1,'a']
const [num, str] = tuple // num-1 str-'a'
枚举
- 枚举可以给几个数值取上容易理解的名字
- 一个枚举中只会存在几个固定的值,不会出现超出范围的可能性
- js中可以使用对象去模拟枚举,而ts中有专门的enum类型
export {}
const enum Status{
Default = 0,
Success = 1,
Fail = 2
}
// 没有设置初始值的情况下,默认为第一个元素赋值为 0,后面的元素会在第一个元素的值上递增
enum Status2{
Default,// 0
Success,// 1
Fail// 2
}
// 没有设置初始值的情况下,默认从 0 开始
enum Status3{
Default = 6,// 6
Success,// 7
Fail// 8
}
函数类型
- 函数声明,设置可选参数可以使用
a?
或者 函数默认值b: number=10
,不确定数量的可以用ES6的…args
export {} // 确保跟其它示例没有成员冲突
// 参数数量在调用时 必须保持一致
function func1 (a: number, b: number = 10, ...rest: number[]): string {
return 'func1'
}
- 函数表达式
const func2: (a: number, b: number) => string = function (a: number, b: number): string {
return 'func2'
}
任意类型
使用 any 接收任意类型参数,依然是动态类型。不会有类型检查,依然有类型安全问题
const a: any = 'abc'
隐式类型推断
如果我们没有给一个变量添加类型注解,那么TS会根据这个变量的使用,去推断它的类型。建议还是给每一个都添加类型注解
类型断言
在某些情况下 TS 无法在编译过程中 知道一个运算的值是什么类型,从而导致后续的代码编译时报错
那么此时我们可以使用断言 as 来确定类型
export {} // 确保跟其它示例没有成员冲突
// 假定这个 nums 来自一个明确的接口
const nums = [110, 120, 119, 112]
const res = nums.find(i => i > 0)
// const square = res * res 这里会提示报错 因为ts 认为res是number|undefined
const num1 = res as number
const num2 = <number>res // JSX 下不能使用
接口 interface
接口可以用来约定一个对象的结构,我们使用一个接口,那么就要遵守这个接口全部的约定
export {}
interface person {
name: string
age: number
gender: string
}
// p 这个参数必须要有person接口中的结构
function func(p: person){
console.log(p.name);
}
- 可选成员 给属性后面添加 ? 表示该属性可选
- 只读成员 给属性名前面添加 readonly 该属性在初始化之后就不能再被修改了
- 动态属性
// 可以添加任意属性名为string类型 值为string类型的属性
interface Cache {
// prop 只是一个象征意义上的名字
[prop: string]: string
}
const cache: Cache = {}
cache.foo = 'value1'
cache.bar = 'value2'
TypeScript 中类的基本使用
ts 中类的使用与js中ES6中新增的class 差不多,略有差异。 主要体现在类属性定义上,ts的class中的类属性在使用之前,必须要先声明。
export {}
class Pers {
name: string
age: number
constructor(name: string,age: number){
this.name = name
this.age = age
}
// 定义方法
sayHi( msg: string ):void {
console.log( `hello,I'm ${ this.name }, ${ msg }` )
}
}
访问修饰符
- public 公开的,在属性或者方法前进行修饰,表示可以被外部访问,这也是默认的修饰符
- private 私有的,只能在类的内部访问,实例无法在外部调用
- protected 保护的,可以理解为受限的,只能在类内部或者子类中进行访问
如果constructor添加了private 那么是不能直接在类外部new出实例的,可以在静态方法中来调用new创建实例,并return
class Student {
public name: string
public age: number
private constructor (name: string, age: number) {
this.name = name
this.age = age
}
static create (name: string, age: number) {
return new Student(name, age)
}
}
const jack = Student.create('jack', 18)
添加只读属性,class中添加只读属性跟接口中类似 都是通过readonly来实现,具体语法就是给在属性修饰符之后 属性名之前 添加readonly public readonly name: string
类与接口
ts中提到的接口,跟我们前端开发中经常提到的接口请求的接口,有一定差别。
很多只接触过JavaScript的朋友对于接口的理解,可能是我请求这个接口地址就能获得对应的数据
,而我们ts这里提到的接口,是强类型语言中的一个概念,主要是用来实现多态。
而多态在js中并不需要特地去实现 因为js本身就是弱类型的,它天生就有多态的能力。
ts中的接口是比类更加抽象的一种概念,可以理解为是多个类它们之间共性的一种约定,类实现(implements)了某个接口 那么它的内部就必须拥有这种接口的能力。
举个简单的例子
现在有两个类
一个是人类
一个是动物类
人类 和 动物类 都拥有可以吃 可以跑的 两个能力
所以我们可以把它们共性的能力抽离出来,形成一个 EatandRun 的接口 这里接口里就提供两个能力 eat方法 和 run 方法
那么实现该接口的 人类 和动物类 当中则必须要有这两个方法
那么,问题又来了,有人可能会问 那为什么我不直接封装一个父类 而要使用接口呢?
问的好!这里的人类 和 动物类 他们都有eat 和 run 方法 但是他们两个类 实现eat 和 run 的方式是不一样的,
同样是 eat 方法 人类 的eat 方法 是要把食物做熟吃,动物类是生吃
所以如果是一个父类继承 那么这两个方法的实现方式就被固定了,而接口 只是约束了它们有这样的能力 ,具体的实现 可以由各自类的内部 *实现
export {} // 确保跟其它示例没有成员冲突
interface Eat {
eat (food: string): void
}
interface Run {
run (distance: number): void
}
class Person implements Eat, Run {
eat (food: string): void {
console.log(`优雅的进餐: ${food}`)
}
run (distance: number) {
console.log(`直立行走: ${distance}`)
}
}
class Animal implements Eat, Run {
eat (food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
run (distance: number) {
console.log(`爬行: ${distance}`)
}
}
抽象类
抽象类更像是接口跟父类的结合,既可以在其中定义具体的方法,也可以定义抽象方法交给子类去*实现
抽象类是用来继承的,不能被实例化。抽象类里可以有成员变量,接口中没有。
定义一个抽象类,使用abstract class两关键字定义
// 抽线类
export { } // 确保跟其它示例没有成员冲突
abstract class animate {
uname: string
constructor(uname: string) {
this.uname = uname
}
// 抽象方法 不需要函数体
abstract eat(msg: string): void
// 普通方法
run(): void {
console.log(`${this.uname} is run`);
}
}
class Dog extends animate {
age: number
constructor(uname: string, age: number) {
super(uname)
this.age = age
}
eat(msg: string) {
console.log('狗狗吃' + msg);
}
}
let ww = new Dog('viki',2)
console.log(ww.uname);
ww.run()
ww.eat('肉')
泛型
不确定函数的参数类型 或者返回值类型 可以使用泛型。
T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
K(Key):表示对象中的键类型;
V(Value):表示对象中的值类型;
E(Element):表示元素类型
function createArray<T> (length: number, value: T): T[] {
const arr = Array<T>(length).fill(value)
return arr
}
// const res = createNumberArray(3, 100)
// res => [100, 100, 100]
const res = createArray<string>(3, 'foo')
类型声明 declare
在开发过程中不可避免要引用其他第三方的 JavaScript 的库,虽然通过直接引用可以调用库的类和方法,但是却无法使用TypeScript 诸如类型检查等特性功能。
一般的第三方js插件在npm上都有对应的声明文件, 比如lodash的声明文件就可以在npm上下载 npm i @types/lodash
这样使用的时候导入.
以.d.ts结尾的就是声明文件,这样就不会再报找不到模块声明了
如果第三方插件没有对应的声明文件 我们可以自己去为它的成员添加类型声明 从而添加类型检查
import { camelCase } from 'lodash'
declare function camelCase (input: string): string
const res = camelCase('hello typed')