JS 是一门动态弱类型语言,与之对应,自然有静态类型语言和强类型语言。如何区分它们,是在学习一门计算机语言之前首先要搞清楚的问题,理解了这些概念,你就会明白为什么有语言鄙视链,也会对 TS 的诞生的原因有一定的认识。
类型基础 强类型语言与弱类型语言 什么是强类型语言? 人们对这个概念有不同的理解与定义。
比较早的定义,在强类型语言中,当一个对象从调用函数传递到被调用函数时,其类型必须与被调用函数中声明的类型兼容。
1 2 3 4 5 6 A ( ) { B(x) } b (y ) { }
这是比较早的定义,约束不是那么严格,后面我们一般这么定义:
强类型语言:不允许改变变量的数据类型,除非进行强制类型转换
1 2 3 4 5 6 7 8 9 class C { public static void main (String[] args) { int x = 1 ; boolean y = true ; x = y; System.out.println(x); } }
这样代码会报错,因为两者类型不同:
1 2 3 4 char z = 'a' ;x = z; System.out.println(x);
这里 java 进行了强制类型转换,这样 x 就保持了整形
什么是弱类型语言?
弱类型语言:变量可以被赋予不同的数据类型
1 2 3 4 let x = 1 let y = true x = y console .log(x)
我们可以看到 x 变成了 true
1 2 3 4 5 let x = 1 let y = true let z = 'a' x = z console .log(x)
这里 x 又变成了字符串
从这里可以看出 JS 是一门弱类型语言
小结 强类型语言中,变量类型转换有严格限制,不同类型的变量还不能相互赋值的,这样就能避免很多类型造成的错误。弱类型语言则没什么约束,虽然运用更灵活,但是更容易出现错误。
静态类型语言与动态类型语言
静态类型语言:在编译阶段确定所有变量的类型
动态类型语言:在执行阶段确定所有变量的类型
JS 编译器看到这段代码时,无法确定 a
和 b
的类型,只有程序执行时根据传进来的对象才能确定。
C++ 在编译的时候就能确定变量的类型了。
JavaScript 与 C++ 对比
由此看出,动态类型语言,无论是在时间还是空间上都有较多的损耗。
对比总结
其他定义 强类型语言:不允许程序在发生错误后继续执行
争议:C/C++ 就不是上面所说的强类型语言,没有对数组越界进行检查
小结 语言类型象限
问题:TypeScript 是什么类型语言?
TS是强类型,面向对象,编译型的语言。
基本类型 正式开始 TypeScript 程序编写(Helloworld)
准备一个 ts 目录然后用 vscode 编辑他:
创建 TypeScript 项目
打开控制台,创建 package.json
安装 TypeScript(全局)
查看 tsc 帮助信息验证安装
我们可以看到很多配置项目,这些项目可以通过一个配置文件来实现
创建配置项 tsconfig.json
这个文件有完整的注释
新建目录创建文件
1 2 3 mkdir src cd scr touch index.ts
编写文件
1 let hello : string = 'hello typescript'
编译这个文件
我们可以看到一个新的 JavaScript 文件输出了,看一下它的内容
1 var hello = 'hello typescript' ;
除此之外我们还可以通过官网的 playground 来查看编译结果:
输出的结果和 tsc 是一样的
配置构建工具 使用 webpack 管理我们的工程(三个包):
1 npm i webpack webpack-cli webpack-dev-server -D
配置 webpack 的时候,需要区分开发环境和生产环境,因为两个环境是不一样的,需要做不一样的配置。为了配置的可维护性,我们可以将开发环境的配置,生产环境的配置还有公共配置分开书写,最后通过插件进行合并。
webpack.config.js
:所有文件的入口
webpack.base.config.js
:公共环境的配置
webpack.dev.config.js
:开发环境的配置
webpack.pro.config.js
:生产环境的配置
公共环境配置 webpack.base.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const HtmlWebpackPlugin = require ('html-webpack-plugin' )module .exports = { entry: './src/index.ts' , output: { filename: 'app.js' }, resolve: { extensions: ['.js' , '.ts' , '.tsx' ] }, module : { rules: [ { test: /\.tsx?$/i , use: [{ loader: 'ts-loader' }], exclude: /node_modules/ } ] }, plugins: [ new HtmlWebpackPlugin({ template: './src/tpl/index.html' }) ] }
首先指定了一个入口文件 ./src/index.ts
然后输出文件名是 app.js
,使用默认的根目录
使用三个扩展名 .js
.ts
.tsx
既然我们引入了新的文件 ts,那就需要使用相应的 loader
,我们安装 ts-loader
:
1 npm i ts-loader typescript -D // 还需要本地安装一下 typescript
ts-loader 的正则:/\.tsx?$/i
,并排除 node_module 下的文件
插件 HtmlWebpackPlugin
:通过一个模板帮助生成网站首页,而且帮助把输出文件自动嵌入到模板文件中,安装一下
1 npm i html-webpack-plugin -D
编写一下模板文件
1 2 3 4 5 6 7 8 9 10 11 12 13 // ./src/tpl/index.html <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > TypeScript</title > </head > <body > <div class ="app" > </div > </body > </html >
开发环境 webpack.dev.config.js
1 2 3 module .exports = { devtool: 'cheap-module-eval-source-map' }
这是官方推荐的配置,
cheap
表示忽略文件的列信息(调试这个没用)
module
表示定位到 TypeScript
源码,而不是经 loader
转义的 JavaScript
源码
eval-source-map
表示会将 source-map
以 dataUrl
的形式打包到文件中,重编译速度是很快的,不用担心性能问题。
生产环境 webpack.pro.config.js
1 2 3 4 5 6 7 const { CleanWebpackPlugin } = require ('clean-webpack-plugin' )module .exports = { plugins: [ new CleanWebpackPlugin() ] }
这个作用是,在每次成功构建之后,帮助我们清空 dist
目录。因为有的时候为了避免缓存,会在文件后加入哈希,在多次构建后会产生很多无用文件。这个插件会帮我们清空目录。我们安装一下:
1 npm i clean-webpack-plugin -D
入口文件 webpack.config.js
1 2 3 4 5 6 7 8 9 const merge = require ('webpack-merge' )const baseConfig = require ('./webpack.base.config' )const devConfig = require ('./webpack.dev.config' )const proConfig = require ('./webpack.pro.config' )module .exports = (env, argv ) => { let config = argv.mode === 'development' ? devConfig : proConfig; return merge(baseConfig, config); };
使用了一个合并插件,我们安装一下:
修改 NPM 脚本 开发环境脚本 1 2 3 4 5 6 7 "main" : "./src/index.ts" , "scripts" : { "start" : "webpack-dev-server --mode=development --config ./build/webpack.config" , "test" : "echo \"Error: no test specified\" && exit 1" },
运行一下 npm start
,我们可以看到效果:
可以在模板 html 中写一些东西:
1 <div class ="app" > 123123</div >
然后就能在 http://127.0.0.1:8080
中看到
生产环境脚本 1 2 3 4 5 6 7 "scripts" : { "build" : "webpack --mode=produnction --config ./build/webpack.config.js" , "start" : "webpack-dev-server --mode=development --config ./build/webpack.config" , "test" : "echo \"Error: no test specified\" && exit 1" },
运行一下 npm run build
:
我们能看在目录中看到 dist
,构建好的 app.js
也嵌入到模板文件中。
基本类型
ES6 有 6 种基本数据类型,还有 3 种引用类型,TypeScript 又新增了几个类型。
类型注解
作用:相当于强类型语言的类型声明
语法:(变量/函数):type
基本数据类型 到创建好的工程目录去,在 src
下创建 datatype.ts
文件,并引入到 index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 let bool: boolean = true let num: number = 123 let str: string = 'abc' let arr1: number [] = [1 , 2 , 3 ]let arr2: Array <number > = [1 , 2 , 3 ]let arr3: Array <number | string > = [1 , 2 , 3 , '4' ]let tuple: [number , string ] = [0 , '1' ]tuple.push(2 ) console .log(tuple) let add = (x: number , y : number ): number => x + ylet compute: (x: number , y: number ) => number compute = (a, b ) => a + b let obj: {x : number , y : number } = {x : 1 , y : 3 }obj.x = 3 let s1: symbol = Symbol ()let s2 = Symbol ()console .log(s1 === s2) let un: undefined = undefined let nu: null = null let noReturn = () => {}let xlet error = () => { throw new Error ('error' ) } let endless = () => { while (true ) {} }
TypeScript 完全覆盖了 ES6 的基本数据类型,并通过 any
实现了对 JS 的兼容
枚举 一个角色判断的例子:
枚举:一组有名字的常量集合
接着之前的工程,新建 enum.ts
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 enum Role { Reporter, Developer, Maintainer, Owner, Guest } console .log(Role.Reporter) console .log(Role.Developer) console .log(Role) enum Message { Success = '成功' , Fail = '失败' } enum Answer { N, Y = 'yes' } enum Char { a, b = Char.a, c = 1 + 3 , d = Math .random(), e = '123' .length, f = 4 } const enum Month { Jan, Feb, Mar } let month = [Month.Jan, Month.Feb] enum E { a, b }enum F { a = 0 , b = 1 }enum G { a= 'apple' , b = 'banana' }let e: E = 3 let f: F = 3 let e1: E.alet e2: E.blet e3: E.a = 1 let g1: G = G.blet g2: G.a = G.a
枚举:帮助我们将程序中不容易记忆的硬编码或者是未来中可能改变的常量抽取出来,提高程序可读性、可维护性
类与接口 对象类型接口 接口可以约束对象,函数以及类的结构和类型,这是一种代码协作的契约,我们必须遵守并且不能改变。我们来看一个对象类型的接口是怎么定义的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 interface List { id: number , name: string } interface Result { data: List[] } function render (result: Result ) { result.data.forEach((val ) => { console .log(val.id, val.name) }) } let result = { data: [ { id : 1 , name : 'A' }, { id : 2 , name : 'B' } ] } render(result)
实际开发中,我们经常会接收到后端传递过来的意料之外的字段,会发生什么呢?
1 2 3 4 5 6 let result = { data: [ { id : 1 , name : 'A' , sex : 'male' }, { id : 2 , name : 'B' } ] }
这时候没有报错,因为鸭式变形法:看上去是,那他就是。换到 TS 上就是,只要传入的对象满足接口的必要条件,那么它就被允许,即使传入多余字段。
有一个例外,如果我们直接传入对象自变量,它就会对额外字段进行类型检查:
1 2 3 4 5 6 7 render( { data: [ { id : 1 , name : 'A' , sex : 'male' }, { id : 2 , name : 'B' } ] })
三种解决方法:
对象自变量赋值给变量
类型断言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 render( { data: [ { id : 1 , name : 'A' , sex : 'male' }, { id : 2 , name : 'B' } ] } as Result) render(<Result> { data: [ { id : 1 , name : 'A' , sex : 'male' }, { id : 2 , name : 'B' } ] } as Result)
字符串索引签名
1 2 3 4 5 interface List { id: number , name: string , [x: string ]: any }
用任意字符串索引 List
接口成员的一些属性:
可选
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 interface List { id: number , name: string , [x: string ]: any , age?: number } ... function render (result: Result ) { result.data.forEach((val ) => { console .log(val.id, val.name) if (val.age) { console .log(val.age) } }) }
只读
1 2 3 4 5 6 interface List { readonly id: number , name: string , [x: string ]: any , age?: number }
可索引接口 当你不确定一个接口中有多少个属性时,就可以使用可索引类型的接口,可索引类型的接口可以用数字索引,也可以是字符串。
1 2 3 4 5 interface StringArray { [index: number ]: string } let chars: StringArray = ['A' , 'B' ]
用任意数字索引 StringArray
都会得到一个 string
,相当于声明了一个字符串数组
1 2 3 4 interface Names { [index: string ]: string , }
用任意 string 索引,得到的都是 string,而且不能再声明 number 类型成员
1 2 3 4 5 interface Names { [index: string ]: string , [z: number ]: string }
索引可以混用,需要注意的是,数字索引签名返回值一定要是字符串索引签名返回值的子类型,因为 JavaScript 会类型转换,将 number 转换为 string
函数类型接口 使用接口定义一个函数,之前用变量声明了一个函数类型,现在我们用接口实现一遍:
1 2 3 4 5 let add: (x: number , y: number ) => number interface Add { (x: number , y : number ): number }
两种方式等价,还有一种更简洁的方式:
1 type Add = {x : number , y : number } => number
这是使用类型别名来声明函数类型
1 2 3 type Add = (x: number , y: number ) => number let add: Add = (a, b ) => a + b
混合类型接口 一个接口既可以定义一个函数,又可以像对象一样,拥有属性和方法
1 2 3 4 5 interface Lib { (): void , version: string , doSomething(): void }
定义完成后我们来实现它:
1 2 3 let lib: Lib = () => {}lib.version = '1.0' lib.doSomething = () => {}
还是报错,这时候就需要使用断言:
1 2 3 let lib: Lib = (() => {}) as Liblib.version = '1.0' lib.doSomething = () => {}
实现了一个混合接口,但是向全局暴露了一个变量 lib
,它是一个单例,如果我们创建了多个 lib,就需要封装一下:
1 2 3 4 5 6 7 8 9 10 11 12 function getLib ( ) { let lib: Lib = (() => {}) as Lib lib.version = '1.0' lib.doSomething = () => {} return lib } let lib1 = getLib()lib1() lib1.doSomething let lib2 = getLib()
接口还可以定义类的结构与类型,之后再讲
函数 函数定义 1 2 3 4 5 6 7 8 9 10 11 function add1 (x: number , y: number ) { return x + y } let add2: (x: number , y: number ) => number type add3 = (x: number , y: number ) => number interface add4 { (x: number , y : number ): number }
我们需要明确指出函数参数的类型与个数,而返回值可以使用 ts 的类型推断。后三种只是函数类型的定义(函数声明),缺少函数体。
可选参数 1 2 3 4 function add5 (x: number , y?: number ) { return y? x + y : x } add5(1 )
要注意的是,可选参数不能在必选参数之前。
参数默认值 类似 ES6 的形式
1 2 3 function add6 (x: number , y = 2 , z: number , q = 1 ) { return x + y + z + q }
需要注意的是,在必选参数之前,默认参数是不可以省略的,必须明确传入 undefined
剩余参数 剩余参数是以数组形式存储的:
1 2 3 function add7 (x: number , ...rest: number [] ) { return x + rest.reduce((p, c ) => p + c ) }
函数重载 静态类型语言中都有这个概念,两个函数如果名称相同,但是参数个数与参数类型不同,那么就实现了函数重载,增强了函数的可读性与可扩展性
TS 的函数重载要求我们先定义一系列名称相同的函数声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 function add8 (...rest: number [] ): number function add8 (...rest: string [] ): string function add8 (...rest: any [] ): any { let first = rest [0] if (typeof first === 'string' ) { return rest .join ('' ) } if (typeof first === 'number' ) { return rest.reduce((p, c ) => p + c) } } add8(1 , 2 , 3 ) add8('a' , 'b' , 'c' )
TS 在重载时会查找重载列表,,所以我们要将容易匹配的函数定义写在前面
类 ES6 引入了 class 关键字,我们终于可以像后端语言一样实现面向对象了,TS 的类覆盖了 ES6 的类,并且引入了一些其他特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Dog { constructor (name: string ) { this .name = name } name: string run ( ) {} } console .log(Dog.prototype)let dog = new Dog('wangcai' )console .log(dog)class Dog { constructor ( ) { this .name = 'wangcai' } name: string run ( ) {} } console .log(Dog.prototype)let dog = new Dog()console .log(dog)
与 ES6 不同的是,我们添加了类型注解,此外还有一个 run() 方法,返回值是 void,还有一个构造函数的返回值,返回 Dog 本身(自动推断),注意两个问题:
无论在 ES 还是 TS 中,类成员的属性都是实例属性而不是原型属性,类成员方法都是实例方法
与 ES 不同的是,实例属性必须要有初始值或者在构造函数中被初始化
继承 1 2 3 4 5 6 7 class Husky extends Dog { constructor (name: string , color: string ) { super (name) this .color = color } color: string }
ES 规定派生类的构造函数必须包含 super()
调用,代表父类实例,this
需要放在构造函数之后
成员修饰符 这是 TS 对 ES 的一种扩展
public 默认修饰符,对所有人可见
1 public name: string = 'dog'
private 只能在类的本身被调用,实例都不行
可以给构造函数加上私有成员属性,作用是这个类既不能被实例化,也不能被继承
protected 受保护成员只能在类或者子类中访问,而不能在实例中访问
可以给构造函数加上受保护属性,作用是这个类只能被继承,也就是实现了一个基类
readonly 只读属性不可以被更改,只读属性一定要被初始化,和实例属性一样的
1 readonly legs: number = 4
构造函数参数 将参数变为了实例属性,这样我们就不用在类中定义了,更简洁
1 2 3 4 5 6 7 class Husky extends Dog { constructor (name: string , public color: string ) { super (name) this .color = color } }
static 类的静态成员
1 2 3 4 5 6 7 8 9 class Dog { constructor (name: string ) { this .name = name } name: string run ( ) {} static food: string = 'bones' }
静态成员只能通过类名来调用,不能通过实例或者子类调用
抽象类 ES 中并没有引入抽象类的概念,所谓抽象类就是只能被继承,不能被实例化的类
1 2 3 4 abstract class Animal { }
抽象类中可以定义具体的方法,子类就不用实现了,达成了方法的复用
抽象类中可以不完成一个方法的具体实现,抽象方法:
抽象类可以抽离出类的共性,从而提高代码的复用率
多态 父类中定义一个抽象方法,多个子类中对这个方法有不同的实现,实现运行时的绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Dog extends Animal { sleep ( ) { console .log('dog sleep' ) } } class Cat extends Animal { sleep ( ) { console .log('cat sleep' ) } } let dog = new Dog()let cat = new Cat()let animals: Animal[] = [dog, cat]animals.forEach(i => { i.sleep() })
this 类型 特殊的 TS 类型,类的成员方法可以直接返回一个 this
,这样就可以很方便地实现链式调用
1 2 3 4 5 6 7 8 9 class WorkFlow { step1 ( ) { return this } step2 ( ) { return this } } new WorkFlow().step1().step2()
这样就实现了 TS 类方法的链式调用
this 的多态
this 既可以是父类型,也可以是子类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class WorkFlow { step1 ( ) { return this } step2 ( ) { return this } } new WorkFlow().step1().step2()class MyFlow extends WorkFlow { next ( ) { return this } } new MyFlow().next().step1().next().step2()
这样就保持了父类与子类间接口调用的连贯性
接口与类的关系 类类型接口 1 2 3 4 5 6 7 8 9 10 11 12 interface Human { name: string , eat(): void } class Asian implements Human { constructor (name: string ) { this .name = name } name: string eat ( ) {} }
一个接口可以约束类有多少属性以及它们的类型,注意点:
类实现接口的时候,必须实现接口中所有属性(类可以添加属性)
接口只能约束类的公有成员
接口不能约束类的构造函数
接口的继承 接口可以像类一样实现继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface Human { name: string , eat(): void } interface Man extends Human { run(): void } interface Child { cry(): void } interface Boy extends Man, Child {}let boy: Boy = { name: '' , run ( ) {}, eat ( ) {}, cry ( ) {} }
接口继承类 相当于把类的成员都抽象了出来,也就是只有结构,没有实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Auto { state = 1 } interface AutoInterface extends Auto { } class C implements AutoInterface { state = 2 } class Bus extends Auto implements AutoInterface { }
接口不仅抽象了公共属性,而且把私有成员和受保护成员都抽象出来了
图解
泛型 希望一个函数或者一个类支持多种数据类型,怎么解决呢?
概念
我们希望接收字符串数组,那么可以使用函数重载来实现:
还可以用联合类型,更简便一些:
我们还不满足,希望接收任何类型参数,any 类型:
但是这样会丢失一些约束信息,也就是类型之间的约束关系,忽略了输入参数与返回值的类型必须一致。这个时候就用到泛型了。
泛型概念:不预先确定的数据类型,具体的类型在使用的时候才能确定
泛型函数与泛型接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function log <T >(value: T ): T { console .log(value) return value } log<string []>(['a' , 'b' ]) log(['a' , 'b' ]) type Log = <T>(value: T ) => Tlet myLog: Log = loginterface Log2 { <T>(value: T): T } interface Log3<T> { (value: T): T } let myLog3: Log3<number > = logmyLog3(1 ) interface Log4<T = string> { (value: T): T }
将泛型变量与函数参数等同对待,只不过是代表值得参数,泛型在高阶函数中有广泛应用。
泛型类与泛型约束 泛型类
1 2 3 4 5 6 7 8 9 10 11 class Log <T > { run (value: T ) { console .log(value) return value } } let log1 = new Log<number >()log1.run(1 ) let log2 = new Log()log2.run({a :1 })
泛型约束
1 2 3 4 5 function log <T >(value: T ): T { console .log(value, value.length) return value }
预定义接口,进行约束:
1 2 3 4 5 6 7 8 9 interface Length { length: number } function log <T extends Length >(value: T ): T { console .log(value, value.length) return value }
不管传入什么,必须要有 length 属性(数组、字符串、集合)
好处
增强程序的可扩展性:函数或类可以轻松支持多种数据类型
增强代码的可读性:不必写多条函数重载,或者冗长的联合类型声明
灵活控制类型之间的约束
到此,基础知识基本介绍结束
TypeScript 进阶 类型检查机制
类型检查机制:TypeScript 编译器在做类型检查时,秉承的一些原则,以及表现出的一些行为
作用:辅助开发,提高效率
类型推断
不用指定变量的类型(函数的返回值类型),TypeScript 可以根据某些规则自动为其推断一个类型
基础类型推断 1 2 3 4 let a = 1 let b = [1 ]let c = (x = 1 ) => x + 1
最佳通用类型推断 需要从多个类型中推断类型时,TS 会尽可能推断一个兼容所有类型的,通用的类型
上下文类型推断 上面两个都是从右到左的推断,上下文是从左到右的推断,通常发生在事件处理中
1 2 3 4 window .onkeydown = (event ) => { console .log(event) }
类型断言 以上三个方法可能不符合你的预期,TS 也提供了一个方法供你覆盖推断类型
1 2 let foo = {}foo.bar = 1
1 2 3 4 5 6 interface Foo { bar: number } let foo = {} as Foofoo.bar = 1
类型断言不能乱用:
1 2 3 4 5 interface Foo { bar: number } let foo = {} as Foo
这时候没有按照接口约定严格拥有属性,怎么办呢?:
1 2 3 4 5 6 interface Foo { bar: number } let foo: Foo = { bar: 1 }
这才是最佳方法
类型兼容性 什么是兼容性
当一个类型 Y 可以赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
X 兼容 Y:X(目标类型) = Y(源类型)
接口间兼容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 interface X { a: any , b: any } interface Y { a: any , b: any , c: any } let x: X = { a : 1 , b : 2 }let y: Y = { a : 1 , b : 2 , c : 3 }x = y y = x
源类型只要有目标类型的必要属性,就可以进行赋值
函数间兼容 常用于函数作为参数回调的场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 type Handler = (a: number , b: number ) => void function hof (handler: Handler ) { return handler } let handler1 = (a: number ) => {}hof(handler1) let handler2 = (a: number , b: number , c: number ) => {}hof(handler2) let a = (p1: number , p2: number ) => {}let b = (p1?: number , p2?: number ) => {}let c = (...args: number [] ) => {}let handler3 = (a: string ) => {}hof(handler3) interface Point3D { x: number , y: number , z: number } interface Point2D { x: number , y: number } let p3d = (point: Point3D ) => {}let p2d = (point: Point2D ) => {}p3d = p2d p2d = p3d let f = () => ({name : 'Alice' })let g = () => ({name : 'Alice' , location : 'shanghai' })f = g g = f function overload (a: number , b: number ): number function overload (a: string , b: string ): string function overload (a: any , b: any ): any {}// function overload (a: any , b: any ) {} // function overload (a: any , b: any , c: any ): any {}
枚举兼容性 1 2 3 4 5 6 7 8 enum Fruit { Apple, Banana }enum Color { Red, Blue }let fruit: Fruit.Apple = 3 let no: number = Fruit.Apple let color: Color.Red = Fruit.Apple
类兼容性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class A { constructor (a: number , b: number ) {} id: number = 1 private name: string = '' } class B { static s = 1 constructor (p: number ) {} id: number = 2 private name: string = '' } let aa = new A(1 , 2 )let ab = new B(1 )aa = ab ab = aa class D extends A {}let ad = new D(1 , 2 )aa = ac ac = aa
泛型兼容性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface Empty<T> {} let o1: Empty<number > = {}let o2: Empty<string > = {}o1 = o2 interface Empty2<T> { value: T } let o3: Empty2<number > = {1 }let o4: Empty2<string > = {'a' }o3 = o4 let loga = <T>(x: T): T => { return x } let logb = <U>(y: U): U => { return y } loga = logb
口诀
结构之间兼容:成员少的兼容成员多的
函数之间兼容:参数多的兼容参数少的
类型保护 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 enum Type {Strong, Week}class Java { helloJava ( ) { console .log('Hello Java' ) } } class JavaScript { helloJavaScript ( ) { console .log('Hello JavScript' ) } } function getLanguage (type : Type ) { let lang = type === Type.Strong ? new Java() : new JavaScript() if ('helloJava' in lang) { lang.helloJava() } else { lang.helloJavaScript() } return lang } getLanguage(Type.Strong)
TypeScript 能够在特定区块中保证变量属于某确定类型
可以在此区块中放心地引用此类型的属性,或者调用此类型的方法
四种创建区块的方法 1 2 3 4 5 6 if (lang instanceof Java) { lang.helloJava() } else { lang.helloJavaScript() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Java { helloJava ( ) { console .log('Hello Java' ) } java: any } class JavaScript { helloJavaScript ( ) { console .log('Hello JavScript' ) } javascript: any } function getLanguage (type : Type ) { let lang = type === Type.Strong ? new Java() : new JavaScript() if ('java' in lang) { lang.helloJava() } else { lang.helloJavaScript() } return lang }
1 2 3 4 5 6 7 8 9 10 function getLanguage (type : Type, x: string | number ) { if (typeof x === 'string' ){ x.length } else { x.toFixed(2 ) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function isJava (lang: Java | JavaScript ): lang is Java { return (lang as Java).helloJava !== undefined } function getLanguage (type : Type ) { let lang = type === Type.Strong ? new Java() : new JavaScript() if (isJava(lang)) { lang.helloJava() } else { lang.helloJavaScript() } return lang } getLanguage(Type.Strong)
高级类型 高级类型就是指 TS 为了保证语言的灵活性,所引入的一些语言特性(类似语法糖?)
交叉类型 将多个类型合并成一个类型,新的类型将具有所有类型的特性,交叉类型特别适合对象混入的场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 interface DogInterface { run(): void } interface CatInterface { jump(): void } let pet: DogInterface & CatInterface = { run ( ) {}, jump ( ) {} } class Dog implements DogInterface { run ( ) {}, eat ( ) {} } class Cat implements CatInterface { jump ( ) {} eat ( ) {} } enum Master { Boy, Girl }function getPet (master: Master ) { let pet = master === Master.Boy ? new Dog() : new Cat pet.eat() return pet }
我们看到这个就是交叉类型(所有类型的并集):
1 2 3 4 let pet: DogInterface & CatInterface = { run ( ) {}, jump ( ) {} }
联合类型 明确概念:声明的类型并不确定,可以为其中的一个
1 2 3 4 let a: number | string = 'a' let b: 'a' | 'b' | 'c' let c: 1 | 2 | 3
1 2 3 4 5 function getPet (master: Master ) { let pet = master === Master.Boy ? new Dog() : new Cat pet.eat() return pet }
可区分的联合类型 这种模式本质上讲,是一种结合了联合类型与自身变量的类型保护方法。一个类型如果是多个类型的联合类型,且每个类型之间有一个公共属性,我们就可以借助这个公共属性,创建不同的类型保护区块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 interface Square { kind: 'square' , size: number } interface Rectangle { kind: 'rectangle' , width: number , height: number } type Shape = Square | Rectanglefunction area (s: Shape ) { switch (s.kind) { case 'square' : return s.size * s.size case 'rectangle' : return s.width * s.height } }
如果我们想要加新的 Shape
,就可能有一些隐患:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 interface Square { kind: 'square' , size: number } interface Rectangle { kind: 'rectangle' , width: number , height: number } interface Circle { kind: 'circle' , r: number } type Shape = Square | Rectangle | Circlefunction area (s: Shape ) { switch (s.kind) { case 'square' : return s.size * s.size case 'rectangle' : return s.width * s.height } }
我们可以打印:
1 console .log(area({kind : 'circle' , r : 1 })
返回一个 undefined
,这样没有约束是不好的,我们修改一下:
1 2 function area (s: Shape ):number
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Shape = Square | Rectangle | Circlefunction area (s: Shape ) { switch (s.kind) { case 'square' : return s.size * s.size case 'rectangle' : return s.width * s.height case 'circle' : return s.r * s.r * 3.14 / 2 default : return ((e: never ) => { throw new Error (e) })(s) } }
索引类型 常见场景:
1 2 3 4 5 let obj = { a: 1 , b: 2 , c: 3 }
抽取值,形成数组:
1 2 3 4 5 function getValues (obj: any , keys: string [] ) { return keys.map(key => obj[key]) } console .log(getValues(obj, ['a' , 'b' ])) console .log(getValues(obj, ['a' , 'd' ]))
但是编译器并没有报错,我们使用索引类型进行约束,我们先要了解几个概念
索引类型的查询操作符 keyof T
:表示类型 T 的所有公共属性的自变量的联合类型
1 2 3 4 5 interface Obj { a: number , b: number } let key: keyof Obj
索引访问操作符 T[K]
:表示对象 T 的属性 K 所代表的类型
泛型继承约束 T extends U
:泛型变量通过集成某个类型,获得某些属性
我们现在来改造函数:
1 2 3 function getValues <T , K extends keysof T >(obj: T, keys: K[] ): T [K ][] { return keys.map(key => obj[key]) }
这时候 TS 类型检查就起作用了
索引类型能实现对对象属性的查询访问,然后泛型约束,就能建立对象、对象属性及属性值之间的约束关系
映射类型 通过映射类型可以从旧的类型生成新的类型,把一个类型中的所有属性变为只读
1 2 3 4 5 6 7 interface Obj { a: string , b: number , c: boolean } type ReadonlyObj = Readonly<Obj>
这是如何实现的呢?我们跳转到实现方法:
1 2 3 type Readonly<T> = { readonly [P in keyof T]: T[P]; };
索引签名是 P in keyof T
:执行了一次遍历操作,P 依次绑定到 T 的属性上
索引签名返回值是索引访问操作符 T[P]
:属性 P 所指定的类型
最后加上 readonly,就将所有属性变为了只读
把一个接口的所有属性变为可选的:
1 type PartialObj = Partial<Obj>
1 2 3 type Partial<T> = { [P in keyof T]?: T[P]; };
pick 映射类型,可以抽取 obj 类型的子集
1 type PickObj = Pick<Obj, 'a' | 'b' >
1 2 3 type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
以上三种类型,官方称之为同态,也就是说它们不会引入新的类型
创建新的属性的映射类型:Record
1 type RecordObj = Record<'x' | 'y' , Obj>
1 2 3 type Record<K extends keyof any , T> = { [P in K]: T; };
条件类型 条件类型是一种由条件表达式所决定的类型,形式如下:
如果类型 T 可以赋值给类型 U,那么结果类型就是 X 类型,条件类型使类型具有了不唯一性,同样的增加了类型的灵活性。
1 2 3 4 5 6 7 type TypeName<T> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends undefined ? 'undefined' : T extends Function ? 'function' : 'object' ;
这是一种条件类型的嵌套,依次判断 T 的类型,然后返回不同字符串
1 2 type t1 = TypeName<string > type t2 = TypeName<string []>
介绍一种分布式条件类型:
1 (A | B) extends U ? X : Y
这时候结果类型会变成多个条件类型的联合类型:
1 (A extends U ? X : Y) | (B extends U ? X : Y)
1 type t3 = TypeName<string | string[]>
利用这个特性可以帮助我们完成类型的过滤
1 type Diff<T, U> = T extends U ? never : T
1 type t4 = Diff<'a' | 'b' | 'c', 'a' | 'e'>
最终 t4 的类型就变成了 b 和 c 的联合类型
继续扩展:从类型中去除不需要的类型,undefined 与 null:
1 2 type NotNull<T> = Diff<T, undefined | null >type TS = NotNull<string | number | undefined | null >
以上两个类型,官方已经实现了:
Diff:Exclude<T, U>
过滤
NoutNull:NonNulable<T>
Diff 相反作用Extract<T, U>
,不是过滤而是抽取(交集)
介绍一种与以上实现不太一样的类型 ReturnType<T>
,获得函数返回值的类型
1 type t7 = ReturnType<() => string >
1 type ReturnType<T extends (...args: any ) => any > = T extends (...args: any ) => infer R ? R : any ;
infer
关键字:代推断,延迟推断