TypeScript官网
1. 基础认知
1.1. JavaScript一门优秀的语言
任何新技术的出现都是为了解决原有技术的某个痛点
JavaScript是一门优秀的编程语言吗?
- 每个人可能观点并不完全一致,但是从很多角度来看,JavaScript是一门非常优秀的编程语言;
- 而且,可以说在很长一段时间内这个语言不会被代替,并且会在更多的领域被大家广泛使用;
著名的Atwood定律
- Stack Overflow的创立者之一的 Jeff Atwood 在2007年提出了著名的 Atwood定律
- any application that can be written in JavaScript, will eventually be written in JavaScript
- 任何可以使用JavaScript来实现的应用都最终都会使用JavaScript实现
这句话正在一步步被应验
Web端
的开发一直都是使用JavaScript;移动端
开发可以借助于ReactNative、Weex、Uniapp
等框架实现跨平台开发;小程序端
的开发也是离不开JavaScript;桌面端
应用程序借助于Electron
来开发;服务器端
开发可以借助于Node
环境使用JavaScript来开发。
1.2. JavaScript的痛点
并且随着近几年前端领域的快速发展,让JavaScript迅速被普及和受广大开发者的喜爱,借助于JavaScript本身的 强大,也让使用JavaScript开发的人员越来越多
优秀的JavaScript没有缺点吗?
- 其实上由于各种历史因素,JavaScript语言本身存在很多的缺点;
- 比如ES5以及之前的使用的
var关键字
关于作用域的问题; - 比如最初JavaScript设计的数组类型并不是连续的内存空间;
- 比如直到今天JavaScript也没有加入
类型检测
这一机制;
JavaScript正在慢慢变好
- 不可否认的是,JavaScript正在慢慢变得越来越好,无论是从底层设计还是应用层面
- ES6、7、8等的推出,每次都会让这门语言更加现代、更加安全、更加方便
- 但是知道今天,JavaScript在类型检测上依然是毫无进展
1.3. 类型带来的问题
编程开发中有一个共识:错误出现的越早越好
- 能在写代码的时候发现错误,就不要在代码编译时再发现(IDE的优势就是在代码编写过程中可以错误)
- 能在代码编译期间发现错误,就不要在代码运行期间再发现(类型检测就可以很好的做到这一点)
- 能在开发阶段发现错误,就不要在测试期间发现错误,能在测试期间发现错误,就不要在上线后发现错误。
探究如何在 代码编译期间 发现代码的错误
- JavaScript可以做到吗?
- 不可以,来看下面这段经常可能出现的代码问题
1
2
3
4
5
6
7
8function getLength(Str){
return str.length;
}
// 正确的调用
getLength("abc");
// 错误的调用(IDE并不会报错)
getLength();出现错误❌
1
2
3
4
5
6
7/TypeScript/index.js:2
return str.length;
^
TypeError: Cannot read properties of undefined (reading 'length')
at getLength (/TypeScript/index.js:2:16)
at Object.<anonymous> (/TypeScript/index.js:6:13)
1.4. 类型错误
这是有一个非常常见的错误
- 这个错误很大的原因就是因为JavaScript没有对传入的参数进行任何的限制,只能等到运行期间才发现这个错误;
- 并且当这个错误产生时,会影响后续代码的继续执行,也就是整个项目都因为一个小小的错误而深入崩溃;
当然,怎么可能犯这样低级的错误呢?
- 当像上面这样的简单的demo时,这样的错误很容易避免,并且当出现错误时,也很容易检查出来;
- 但是当开发一个大型项目时呢?能保证一定不会出现这样的问题吗?
- 而且如果是调用别人的类 库,又如何知道传入的到底是什么样的参数呢?
但是,如果可以给JavaScript加上很多限制,在开发中就可以很好的避免这样的问题了
- 比如getLength函数中str是一个必传的类型,没有调用者没有传编译期间就会报错;
- 比如要求它的必须是一个String类型,传入其他类型就直接报错;
- 那么就可以知道很多的错误问题在编译期间就被发现,而不是等到运行时再去发现和修改;
1.5. 类型思维的缺失
简单体会了没有类型检查带来的一些问题,JavaScript因为从设计之初就没有考虑类型的约束问题,所以造成了前端开发人员关于类型思维的缺失
- 前端开发人员通常不关心变量或者参数是什么类型的,如果在必须确定类型时,往往需要使用各种判断验 证;
- 从其他方向转到前端的人员,也会因为没有类型约束,而总是担心自己的代码不安全,不够健壮;
所以经常会说JavaScript不适合开发大型项目,因为当项目一旦庞大起来,这种宽松的类型约束会带来非常多的安全隐患,多人员开发它们之间也没有良好的类型契约
- 比如去实现一个核心类库时,如果没有类型约束,那么需要对别人传入的参数进行各种验证来保证代码的健壮性;
- 比如去调用别人的函数,对方没有对函数进行任何的注释,只能去看里面的逻辑来理解这个函数需要传入什么参数,返回值是什么类型;
1.6. JavaScript添加类型约束
为了弥补JavaScript类型约束上的缺陷,增加类型约束,很多公司推出了自己的方案
- 2014年,Facebook推出了flow来对JavaScript进行类型检查;
- 同年,Microsoft微软也推出了TypeScript1.0版本;
- 他们都致力于为JavaScript提供类型检查;
而现在,无疑TypeScript已经完全胜出
- Vue2.x的时候采用的就是flow来做类型检查;
- Vue3.x已经全线转向TypeScript,98.3%使用TypeScript进行了重构;
- 而Angular在很早期就使用TypeScript进行了项目重构并且需要使用TypeScript来进行开发;
- 而甚至Facebook公司一些自己的产品也在使用TypeScript;
学习TypeScript不仅仅可以为代码增加类型约束,而且可以培养前端程序员具备类型思维
1.7. 认识TypeScript
虽然已经知道TypeScript是干什么的了,也知道它解决了什么样的问题,但是还是需要全面的来认识一下TypeScript到底是什么?
TypeScript在GitHub和官方上对自己的定义
- GitHub说法:TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
- TypeScript官网:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
- 翻译一下:TypeScript是拥有类型的JavaScript超集,它可以编译成普通、干净、完整的JavaScript代码
怎么理解上面的话呢?
- 可以将TypeScript理解成加强版的JavaScript
- JavaScript所拥有的特性,TypeScript全部都是支持的,并且它紧随ECMAScript的标准,所以ES6、ES7、ES8等新语法标准,它都是支持的;
- 并且在语言层面上,不仅仅增加了类型约束,而且包括一些语法的扩展,比如枚举类型(Enum)、元组类型(Tuple)等;
- TypeScript在实现新特性的同时,总是保持和ES标准的同步甚至是领先;
- 并且TypeScript最终会被编译成JavaScript代码,所以并不需要担心它的兼容性问题,在编译时也不需要借助于Babel这样的工具;
- 所以,可以把TypeScript理解成更加强大的JavaScript,不仅让JavaScript更加安全,而且给它带来了诸多好用的特性;
1.8. TypeScript的特点
官方对TypeScript有几段特点的描述
始于JavaScript,归于JavaScript
TypeScript从今天数以百万计的JavaScript开发者所熟悉的语法和语义开始。使用现有的JavaScript代码,包括流行的JavaScript库, 并从JavaScript代码中调用TypeScript代码;
TypeScript可以编译出纯净、 简洁的JavaScript代码,并且可以运行在任何浏览器上、Node.js环境中和任何支持ECMAScript 3(或 更高版本)的JavaScript引擎中;
TypeScript是一个强大的工具,用于构建大型项目
- 类型允许JavaScript开发者在开发JavaScript应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构;
- 类型是可选的,类型推断让一些类型的注释使你的代码的静态验证有很大的不同。类型让你定义软件组件之间的接口和洞察现有 JavaScript库的行为;
拥有先进的 JavaScript
- TypeScript提供最新的和不断发展的JavaScript特性,包括那些来自2015年的ECMAScript和未来的提案中的特性,比如异步功能和Decorators,以帮助建立健壮的组件;
- 这些特性为高可信应用程序开发时是可用的,但是会被编译成简洁的ECMAScript3(或更新版本)的JavaScript;
1.9. 众多项目采用TypeScript
- 正是因为有这些特性,TypeScript目前已经在很多地方被应用
- Angular源码在很早就使用TypeScript来进行了重写,并且开发Angular也需要掌握TypeScript;
- Vue3源码也采用了TypeScript进行重写,在前面阅读源码时我们看到大量TypeScript的语法;
- 包括目前已经变成最流行的编辑器VSCode也是使用TypeScript来完成的;
- 包括在React中已经使用的ant-design的UI库,也大量使用TypeScript来编写;
- 目前公司非常流行Vue3+TypeScript、React+TypeScript的开发模式;
- 包括小程序开发,也是支持TypeScript的;
1.10. 大前端的发展趋势
大前端是一群最能或者说最需要折腾的开发者
- 客户端开发者:从Android到iOS,或者从iOS到Android,到RN,甚至现在越来越多的客户端开发者接触前端相关知识(Vue、React、Angular、小程序);
- 前端开发者:从jQuery到AngularJS,到三大框架并行:Vue、React、Angular,还有小程序,甚至现在也要 接触客户端开发(比如RN、Flutter);
- 目前又面临着不仅仅学习ES的特性,还要学习TypeScript;
- 新框架的出现,我们又需要学习新框架的特性,比如vue3.x、react18等等;
但是每一样技术的出现都会让惊喜,因为他必然是解决了之前技术的某一个痛点的,而TypeScript真是解决了 JavaScript存在的很多设计缺陷,尤其是关于类型检测的
并且从开发者长远的角度来看,学习TypeScript有助于前端程序员培养 类型思维,这种思维方式对于完成大 型项目尤为重要
2. TypeScript配置
2.1. TypeScript的编译环境
TypeScript最终会被编译成JavaScript来运行,所以需要搭建对应的环境
- 需要在电脑上安装TypeScript,这样就可以通过TypeScript的Compiler将其编译成JavaScript
- 编译TypeScript代码 -> 编译后的JavaScript代码 -> 运行
所以,需要先可以先进行全局的安装:
1
npm install typescript -g
编译文件,文件夹内自动生成 index.js
1
tsc index.ts
2.2. TypeScript的运行环境
如果每次为了查看TypeScript代码的运行效果,都通过经过两个步骤的话就太繁琐了:
- 第一步:通过tsc编译TypeScript到JavaScript代码;
- 第二步:在浏览器或者Node环境下运行JavaScript代码;
是否可以简化这样的步骤呢?
- 比如编写了TypeScript之后可以直接运行在浏览器上?
- 比如编写了TypeScript之后,直接通过node的命令来执行?
通过两个解决方案来完成
- 方式一:通过webpack,配置本地的TypeScript编译环境和开启一个本地服务,可以直接运行在浏览器上
- 方式二:通过ts-node库,为TypeScript的运行提供执行环境;
2.3. webpack配置
2.3.1. 创建一个简单的项目目录结构
新建一个新的目录:TypeScript,并且创建如下的目录结构
1 | │ index.html |
目录和文件夹结构分析:
- index.html是跑在浏览器上的模块文件
- build文件夹中用于存放webpack的配置信息
- src用于存放编写的所有TypeScript代码
2.3.2. 使用npm管理项目的依赖
webpack本身需要有很多的依赖,并且也需要启动 node 服务来快速浏览 index.html 模板以及编译后的 JavaScript 代码
使用npm来初始化package.json文件:
1 | npm i webapck webpack-cli -D |
2.3.3. 本地依赖TypeScript
为什么需要本地依赖TypeScript
- 因为通过 webpack 编译TypeScript代码,并不是通过tsc来完成的
- tsc使用的是全局安装的TypeScript依赖
- webpack会在本地去查找TypeScript的依赖,所以需要本地依赖TypeScript;
安装本地TypeScript依赖
1 | npm i typescript -D |
2.3.4. 初始化tsconfig.json文件
在进行TypeScript开发时,会针对TypeScript进行相关的配置,而这些配置信息是存放在一个tsconfig.json文件中
1 | tsc --init |
2.3.5. 在package.json中添加启动命令
1 | { |
2.3.6. 添加webpack依赖
依赖一:ts-loader
因为需要解析.ts文件,所以需要依赖对应的loader:ts-loader
1 | npm i ts-loader -D |
依赖二:html-webpack-plugin
编译后的代码需要对应的html模块作为它的运行环境,所以需要使用html-webpack-plugin来将它插入到对应的模板中:
1 | npm install html-webpack-plugin -D |
依赖三:webpack-dev-server
本地运行项目
1 | npm i webpack-dev-server -D |
2.3.7. 配置webpack.config.js
1 | const {resolve} = require("path"); |
2.3.8. 打包项目
1 | npm run build |
2.3.9. 运行项目
1 | npm run serve |
2.4. 使用ts-node
方式二:安装ts-node
1
npm install ts-node -g
另外ts-node需要依赖 tslib 和 @types/node 两个包
1
npm install tslib @types/node -g
现在,可以直接通过 ts-node 来运行TypeScript的代码
1
ts-node math.ts
3. 变量
3.1. 变量的声明
在TypeScript中定义变量需要指定 标识符 的类型
所以完整的声明格式如下
- 声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解;
var/let/const 标识符: 数据类型 = 赋值;
比如声明一个message,完整的写法如下
let message: string = "Hello World";
注意:这里的string是小写的,和String是有区别的
string是TypeScript中定义的字符串类型,String是ECMAScript中定义的一个类
如果给message赋值其他类型的值,那么就会报错:
1
2// 不能将类型“number”分配给类型“string”。ts(2322)
// message = 123;
3.2. 声明变量的关键字
在TypeScript定义变量(标识符)和ES6之后一致,可以使用var、let、const来定义
1
2
3var name: string = "test";
let age: number = 20;
const height: number = 1.88;当然,在TypeScript中并不建议再使用var关键字了,主要原因和ES6升级后let和var的区别是一样的,var是没 有块级作用域的,会引起很多的问题,这里不再展开探讨。
4. JavaScript和TypeScript的数据类型
4.1. JavaScript数据类型
4.1.1. number
数字类型是开发中经常使用的类型,TypeScript和JavaScript一样,不区分整数类型(int)和浮点型 (double),统一为number类型
1
2
3let num = 100;
num = 20;
num = 6.66;ES6新增了二进制和八进制的表示方法,而TypeScript也是支持二进制、八进制、十 六进制的表示
1
2
3
4
5
6
7
8
9
10
11
12
13let num:number;
// 十进制
num = 100;
console.log(num);
// 二进制
num = 0b110;
console.log(num);
// 八进制
num = 0o555;
console.log(num);
// 十六进制
num = 0xf23;
console.log(num);
4.1.2. boolean
boolean类型只有两个取值:true和false,非常简单
1 | let flag:boolean = true; |
4.1.3. string
string类型是字符串类型,可以使用单引号或者双引号表示:
1
2
3// string 类型表示
let message: string = "Hello World";
message = 'Hello TypeScript';同时也支持ES6的模板字符串来拼接变量和字符串:
1
2
3
4
5
6const name = "test";
const age = 18;
const height = 1.88;
const info = `my name is ${name}, age is ${age}, height is ${height}`;
console.log(info);
4.1.4. Array
数组类型的定义也非常简单,有两种方式:
1
2
3const names1: string[] = ['abc'];
const names2: Array<string> = ['a'];如果添加其他类型到数组中,那么会报错:
1
2// 类型“number”的参数不能赋给类型“string”的参数。ts(2345)
// names1.push(1);
4.1.5. Object
object对象类型可以用于描述一个对象:
1
2
3
4
5const info : Object = {
name: 'test',
age: 18,
height: 1.88
}但是从 info中不能获取数据,也不能设置数据:
1
2
3
4
5console.log(info[name]);
TSError: ⨯ Unable to compile TypeScript:
index.ts:7:13 - error TS7053: Element implicitly has an 'any' type because expression of type '"name"' can't be used to index type 'Object'.
Property 'name' does not exist on type 'Object'.因为Object中并没有name属性,无法通过编译,更何况执行
4.1.6. Symbol
在ES5中,如果不可以在对象中添加相同的属性名称的,比如下面的做法:
1
2
3
4const person = {
identity: "a",
identity: "b"
}通常的做法是定义两个不同的属性名字:比如identity1和identity2
但是也可以通过symbol来定义相同的名称,因为Symbol函数返回的是不同的值
1
2
3
4
5
6
7
8
9const s1: symbol = Symbol("title");
const s2: symbol = Symbol("title");
const person = {
[s1]: "a",
[s2]: "b"
}
console.log(person[s1]);
4.1.7. null和undefined
在 JavaScript 中,undefined 和 null 是两个基本数据类型
在TypeScript中,它们各自的类型也是undefined和null,也就意味着它们既是实际的值,也是自己的类型:
1
2const a: null = null;
const b: undefined = undefined;
4.2. TypeScript数据类型
4.2.1. any
在某些情况下,无法确定一个变量的类型,并且可能它会发生一些变化,这个时候可以使用any类型(类似 于Dart语言中的dynamic类型)。
any类型有点像一种讨巧的TypeScript手段
- 可以对any类型的变量进行任何的操作,包括获取不存在的属性、方法;
- 给一个any类型的变量赋值任何的值,比如数字、字符串的值;
如果对于某些情况的处理过于繁琐不希望添加规定的类型注解,或者在引入一些第三方库时,缺失了类型注解,这个时候 我们可以使用any
- 包括在Vue源码中,也会使用到any来进行某些类型的适配;
1 | let num: any = 123; |
4.2.2. unknown
unknown是TypeScript中比较特殊的一种类型,它用于描述类型不确定的变量
1 | function foo():string{ |
unknown类型只能赋值给any和unknown类型
1 | const a: unknown = 123; |
any类型可以赋值给任意类型
1 | const c: any = '123'; |
4.2.3. void
- void通常用来指定一个函数是没有返回值的,那么它的返回值就是void类型
- 可以将null和undefined赋值给void类型,也就是函数可以返回null或者undefined
- 这个函数没有写任何类型,那么它默认返回值的类型就是void的,也可以显示的来指定返回值是void
1 | function sum(a:number,b:number):void{ |
4.2.4. never
never 表示永远不会发生值的类型,比如一个函数
- 如果一个函数中是一个死循环或者抛出一个异常,那么这个函数会返回东西吗?
- 不会,那么写void类型或者其他类型作为返回值类型都不合适,就可以使用never类型;
1
2
3
4
5
6
7
8
9
10
11function loopFun():never{
// 死循环
while(true){
console.log(123);
}
}
function loopErr():never{
// 抛出异常
throw new Error();
}never有什么样的应用场景呢?
它用到了联合类型,封装一个核心函数时,必须对所需类型进行单独处理,因为default不会被执行
1
2
3
4
5
6
7
8
9
10
11
12function handleMessage(message: number|string){
switch(typeof message){
case 'string':
console.log('foo');
break;
case 'number':
console.log('bar');
break;
default:
const check: never = message;
}
}
4.2.5. tuple
tuple是元组类型,很多语言中也有这种数据类型,比如Python、Swift等
那么tuple和数组有什么区别呢?
首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中。(可以放在对象或者元组中)
其次,元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型;
1 | const info1: (string|number) [] = ['abc',18,1.88]; |
5. Tuple的应用场景
- 那么tuple在什么地方使用的是最多的呢?
- tuple通常可以作为返回的值,在使用的时候会非常的方便;
1 | function useState<T>(state:T){ |
6. 函数
6.1. 参数类型
- 函数是JavaScript非常重要的组成部分,TypeScript允许指定函数的参数和返回值的类型
- 参数的类型注解
- 声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型
1 | function greet(name:string){ |
6.1.1. 可选类型
对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?:
1 | function printCoordinate(point: {x:number,y?:number}){ |
可选类型可以看做是 类型 和 undefined 的联合类型
1 | function print(message?:string){ |
可选类型需要在必传参数的后面
1 | // (parameter) y: number |
6.1.2. 默认参数
- 从ES6开始,JavaScript是支持默认参数的,TypeScript也是支持默认参数的
- 这个时候y的类型其实是 undefined 和 number 类型的联合
1 | function foo(x:number,y:number=6){ |
6.1.3. 剩余参数
从ES6开始,JavaScript也支持剩余参数,剩余参数语法允许将一个不定数量的参数放到一个数组中
1 | function sum(...nums: number[]):number{ |
6.2. 返回值类型
添加返回值的类型注解,这个注解出现在函数列表的后面:
和变量的类型注解一样,通常情况下不需要返回类型注解,因为TypeScript会根据 return 返回值推断函数的返回类型
- 某些第三方库处于方便理解,会明确指定返回类型,但是这个看个人喜好;
1 | function sum(num1:number, num2:number):number{ |
6.3. 匿名函数的参数
匿名函数与函数声明会有一些不同:
当一个函数出现在TypeScript可以确定该函数会被如何调用的地方时;
该函数的参数会自动指定类型;
1
2
3
4
5const names = ['a','b','c'];
names.forEach(item=>{
item.toUpperCase();
});
并没有指定item的类型,但是item是一个string类型:
- 这是因为TypeScript会根据forEach函数的类型以及数组的类型推断出item的类型;
- 这个过程称之为
上下文类型
(contextual typing),因为函数执行的上下文可以帮助确定参数和返回值的类型;
6.4. 对象类型
如果希望限定一个函数接受的参数是一个对象,这个时候要如何限定呢?
- 可以使用对象类型;
1
2
3
4
5
6function printCoordinate(point: {x:number,y:number}){
console.log("x坐标",point.x);
console.log("y坐标",point.y);
}
printCoordinate({x:10,y:20});使用了一个对象来作为类型:
- 可以添加属性,并且告知TypeScript该属性需要是什么类型;
- 属性之间可以使用 , 或者 ; 来分割,最后一个分隔符是可选的;
- 每个属性的类型部分也是可选的,如果不指定,那么就是any类型;
6.5. 联合类型
TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型
使用第一种组合类型的方法:联合类型(Union Type)
- 联合类型是由两个或者多个其他类型组成的类型;
- 表示可以是这些类型中的任何一个值;
- 联合类型中的每一个类型被称之为联合成员(union’s members);
传入给一个联合类型的值是非常简单的
只要保证是联合类型中的某一个类型的值即可
但是拿到这个值之后,应该如何使用它呢?因为它可能是任何一种类型
比如我们拿到的值可能是string或者number,我们就不能对其调用string上的一些方法;
那么我们怎么处理这样的问题呢?
- 需要使用缩小(narrow)联合;
- TypeScript可以根据我们缩小的代码结构,推断出更加具体的类型;
1 | function printId(id: number|string){ |
6.6. 类型别名
1 | type Point = { |
6.7. 类型断言 as
有时候TypeScript无法获取具体的类型信息,这个需要使用类型断言(Type Assertions)
- 比如通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const ele = document.getElementById("myIng") as HTMLImageElement;
ele.src = "图片地址";
class Person{
}
class Student extends Person{
studying(){
}
}
function sayHello(p:Person){
// 无法使用 p.studying();
(p as Student).studying();
}
const stu = new Student();
sayHello(stu);TypeScript只允许类型断言转换为
更具体
或者不太具体
的类型版本,此规则可防止不可能的强制转换1
2
3// 类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。
// 如果这是有意的,请先将表达式转换为 "unknown"。
const name = 'test' as number;
6.8. 非空类型断言!
编写下面的代码时,在执行ts的编译阶段会报错
这是因为传入的message有可能是为undefined的,这个时候是不能执行方法的;
1
2
3
4
5
6function printMessage(message?:string){
// 对象可能为“未定义”。ts(2532)
console.log(message.toUpperCase());
}
printMessage("hello");
但是,确定传入的参数是有值的,这个时候可以使用非空类型断言:
- 非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测;
1
2
3function printMessage(message?:string){
console.log(message!.toUpperCase());
}
6.9. TypeScript函数类型
在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)
那么在使用函数的过程中,函数是否也可以有自己的类型呢?
- 可以编写函数类型的表达式(Function Type Expressions),来表示函数类型;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16type CalcFunc = (num1:number, num2:number)=>void
function calc(fn:CalcFunc){
console.log(fn(20,30));
}
function sum(num1:number,num2:number){
return num1+num2;
}
function mul(num1:number,num2:number){
return num1*num2;
}
calc(sum);
calc(mul);在上面的语法中 (num1: number, num2: number) => void,代表的就是一个函数类型
- 接收两个参数的函数:num1和num2,并且都是number类型;
- 并且这个函数是没有返回值的,所以是void;
在某些语言中,可能参数名称num1和num2是可以省略,但是TypeScript是不可以的:
6.10. 可选链的使用
- 可选链事实上并不是TypeScript独有的特性,它是ES11(ES2020)中增加的特性:
- 可选链使用可选链操作符 ?.;
- 它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,那么才会继续执行;
- 虽然可选链操作是ECMAScript提出的特性,但是和TypeScript一起使用更版本;
1 | type Person = { |
7. ??和!!的作用
7.1. !!操作符
- 将一个其他类型转换成boolean类型;
- 类似于Boolean(变量)的方式;
1 | const str = 'Hello World'; |
7.2. ??操作符
- 它是ES11增加的新特性;
- 空值合并操作符 (??)是一个逻辑操作符,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数, 否则返回左侧操作数;
1 | const str = 'Hello World'; |
8. 字面量
8.1. 字面量类型
使用字面量类型(literal types)
1
2
3
4let message: 'Hello' = 'Hello';
// 不能将类型“"你好"”分配给类型“"Hello"”
// message = '你好';那么这样做有什么意义呢?
- 默认情况下这么做是没有太大的意义的,但是可以将多个类型联合在一起;
1
2
3
4
5
6
7type Alignment = 'left'|'right'|'center';
function changeAlign(align: Alignment){
console.log("修改方向:" +align);
}
changeAlign("left");
8.2. 字面量推理
来看下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type Method = "GET"|"POST";
function request(url:string,method:Method){
console.log(url,method);
}
type Request = {
url: string,
method: Method
}
const info: Request = {
url: 'https://www.baidu.com',
method: 'GET'
};
request(info.url,info.method);
export {}字面量赋值方式一:
1
request(info.url,info.method as Method);
字面量赋值方式二:
1
2
3
4
5
6
7
8
9
10
11const info = {
url: 'https://www.baidu.com',
method: 'GET'
} as const;
// 等价于
const info: {
readonly url: "https://www.baidu.com";
readonly method: "GET";
}
8.3. 字面量赋值
age不能被赋值
1 | interface Person { |
p却可以指向obj
1 | interface Person { |
- 这是因为TypeScript在字面量
直接赋值
的过程中,为了进行类型推导会进行严格的类型限制
- 但是之后如果是将一个变量标识符
赋值
给其他的变量时,会进行freshness擦除
操作
- 但是之后如果是将一个变量标识符
9. 类型缩小
什么是类型缩小呢?
- 类型缩小的英文是 Type Narrowing;
- 可以通过类似于 typeof padding === “number” 的判断语句,来
改变TypeScript的执行路径
; - 在给定的执行路径中,可以缩小比声明时更小的类型,这个过程称之为 缩小;
- 而编写的 typeof padding === “number 可以称之为 类型保护(type guards);
常见的类型保护有如下几种:
- typeof
- 平等缩小(比如===、!==)
- instanceof
- in
- 等等…
9.1. typeof
在 TypeScript 中,检查返回的值typeof是一种类型保护:因为 TypeScript 对如何typeof操作不同的值进行编码
1 | function printId(id: number|string){ |
9.2. 平等缩小
可以使用switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != )
1 | type Direction = 'left'|'right'|'top'|'bottom'; |
9.3. instanceof
JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”
1 | function printValue(date:Date|string){ |
9.4. in
- Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符
- 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true;
1 | type Fish = { |
10. this
10.1. 可推导的this类型
this是JavaScript中一个比较难以理解和把握的知识点
- this:https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA;
- 因为this在不同的情况下会绑定不同的值,所以对于它的类型就更难把握了;
那么,TypeScript是如何处理this呢?
1
2
3
4
5
6const info = {
name: 'test',
sayHello(){
console.log(this.name);
}
}上面的代码是可以正常运行的,也就是TypeScript在编译时,认为this是可以正确去使用的
- TypeScript认为函数 sayHello 有一个对应的this的外部对象 info,所以在使用时,就会把this当做该对象
10.2. 不确定的this类型
但是对于某些情况来说,并不知道this到底是什么?
1
2
3
4
5
6
7
8function sayHello(){
console.log(this.name);
}
const info = {
name: 'test',
sayHello
}这段代码运行会报错的
- 再次强调一下,TypeScript进行类型检测的目的是
让代码更加的安全
; - 所以这里对于 sayHello 的调用来说,虽然将其放到了info中,通过info去调用,this依然是指向info对象的;
- 但是对于TypeScript编译器来说,这个代码是非常不安全的,因为也有可能直接调用函数,或者通过别的对象来调用函数;
- 再次强调一下,TypeScript进行类型检测的目的是
10.3. 指定this的类型
这个时候,通常TypeScript会要求明确的指定this的类型
1 | type NameType={ |
11. 重载
11.1. 函数的重载
在TypeScript中,如果编写了一个add函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?
可能会这样来编写,但是其实是错误的
1
2
3
4// 运算符“+”不能应用于类型“string | number”和“string | number”
function sum(num1:number|string,num2:number|string):number|string{
return num1+num2;
}那么这个代码应该如何去编写呢
- 在TypeScript中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用;
- 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现;
1
2
3
4
5
6
7
8
9function sum(num1:number,num2:number):number;
function sum(num1:string,num2:string):string;
function sum(num1:any,num2:any):any
{
return num1+num2;
}
console.log(sum('a','b'));
console.log(sum(1,2));
11.2. 联合类型和重载
现在有一个需求:定义一个函数,可以传入字符串或者数组,获取它们的长度
这里有两种实现方案
方案一:使用联合类型来实现;
1
2
3function getLength(a:string|any[]){
return a.length;
}方案二:实现函数重载来实现;
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
73function getLength(a:string):number;
function getLength(a:any[]):number;
function getLength(a:any){
return a.length;
}
```ts
- 在开发中选择使用哪一种呢?
- 在可能的情况下,尽量选择使用联合类型来实现;
# 类
## 认识类的使用
- 在早期的JavaScript开发中(ES5)需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以更加方便的定义和使用类
- TypeScript作为JavaScript的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测
- 实际上在JavaScript的开发过程中,更加习惯于`函数式编程`
- 比如React开发中,目前更多使用的函数组件以及结合Hook的开发模式;
- 比如在Vue3开发中,目前也更加推崇使用 Composition API;
- 但是在封装某些业务的时候,类具有更强大封装性
- 类的定义通常会使用class关键字
- 在面向对象的世界里,任何事物都可以使用类的结构来描述;
- 类中包含特有的属性和方法;
## 类的定义
- 定义一个Person类
- 使用class关键字来定义一个类;
- 可以声明一些类的属性:
- 在类的内部声明类的属性以及对应的类型
- 如果类型没有声明,那么它们默认是any的;
- 也可以给属性设置初始化值;
- 在默认的strictPropertyInitialization模式下的属性是`必须初始化`的,如果没有初始化,那么编译时就会报错;
- 如果在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用 name!: string语法;
- 类可以有自己的构造函数constructor,当我们通过new关键字创建一个 实例时,构造函数会被调用;
- 构造函数不需要返回任何值,默认返回当前创建出来的实例;
- 类中可以有自己的函数,定义的函数称之为方法;
```ts
class Person{
name: string;
age: number;
constructor(name:string,age:number){
this.name = name;
}
running(){
console.log(this.name+" running");
}
eating(){
console.log(this.name+" eating");
}
}
const p = new Person("test",18);
p.eating();
11.3. 类的继承
- 面向对象的其中一大特性就是继承,继承不仅仅可以减少代码量,也是多态的使用前提
- 使用extends关键字来实现继承,子类中使用super来访问父类
- Student类继承自Person
- Student类可以有自己的属性和方法,并且会继承Person的属性和方法
- 在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化;
1 | class Student extends Person{ |
11.4. 多态
父类引用指向子类对象
1 | class Animal{ |
11.5. 类的成员修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;
- private 修饰的是仅在同一类中可见、私有的属性或方法;
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
public是默认的修饰符,也是可以直接访问的
1 | class Person{ |
11.6. 只读属性readonly
如果有一个属性不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly
1 | class Person{ |
11.7. getters/setters
- 私有属性是不能直接访问的,可以使用存取器监听它的获取(getter)和设置(setter)的过程
1 | class Person{ |
11.8. 静态成员
- 在类中定义的成员和方法都属于对象级别的, 在开发中, 有时候也需要定义
类级别的成员和方法
- 在TypeScript中通过关键字static来定义:
1 | class Student{ |
11.9. 类的类型
类本身也是可以作为一种数据类型的
1 | class Person{ |
12. 抽象类abstract
继承是多态使用的前提
- 所以在定义很多通用的调用接口时, 通常会让调用者传入父类,通过多态来实现更加灵活的调用方式
- 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,可以定义为抽象方法
什么是抽象方法?
- 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法
- 抽象方法,必须存在于抽象类中;
- 抽象类是使用abstract声明的类;
抽象类有如下的特点:
- 抽象类是不能被实例的话(也就是不能通过new创建)
抽象方法必须被子类实现
,否则该类必须是一个抽象类;
1 | // 抽象类 |
13. 接口
13.1. 接口的声明
通过type可以用来声明一个对象类型
1
2
3
4type Point = {
x:number,
y:number
}对象的另外一种声明方式就是通过接口来声明
1
2
3
4interface Point {
x:number
y:number
}interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
- 如果是定义
非对象类型
,通常推荐使用type
,比如Direction、Alignment、一些Function; - 如果是定义
对象类型
,那么是有区别的:interface
可以重复
的对某个接口来定义属性和方法;- 而type定义的是别名,别名是不能重复的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18interface Person {
name:string,
running: ()=>void
}
interface Person{
age:number
}
type Animal = {
name: string,
running: ()=>void
}
// 标识符“Animal”重复。ts(2300)
// type Animal = {
// age:number
// }- 如果是定义
13.2. 可选属性
1 | interface Person{ |
13.3. 只读属性
- 接口中也可以定义只读属性
- 这样就意味着我们再初始化之后,这个值是不可以被修改的;
1 | interface Person{ |
13.4. 索引类型
1 | interface FrontLanguage{ |
13.5. 函数类型
通过interface也可以用来定义函数类型
1
2
3
4
5
6
7interface CalcFunc{
(num1:number,num2:number):number;
}
const add:CalcFunc = (num1,num2) =>{
return num1+num2;
}当然,除非特别的情况,还是推荐大家使用类型别名来定义函数
1
type CalcFunc = (num1:number,num2:number)=>number
13.6. 接口继承
- 接口和类一样是可以进行继承的,也是使用extends关键字:
- 并且接口是支持
多继承
的(类不支持多继承)
- 并且接口是支持
1 | interface Person{ |
13.7. 接口的实现
- 接口定义后,也是可以被类实现的
- 如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入;
- 这就是面向接口开发;
1 | interface Swim{ |
14. 交叉类型
联合类型表示多个类型中一个即可
1
type Align = 'left'|'right'|'top'|'bottom';
还有另外一种类型合并,就是交叉类型(Intersection Types)
- 交叉类似表示需要满足多个类型的条件;
- 交叉类型使用 & 符号;
交叉类型
1
type MyType = number&string;
- 表达的含义是number和string要
同时满足
; - 但是有同时满足是一个number又是一个string的值吗?
- 其实是没有的,所以MyType
其实是一个never类型
;
- 表达的含义是number和string要
在开发中,通常是对对象类型进行交叉的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18interface Colorful{
color: string
}
interface Run{
running: ()=>void
}
type NewType = Colorful&Run;
const obj:NewType = {
color: 'blue',
running() {
console.log("running");
}
}
obj.running();
15. 枚举类型
枚举类型是为数不多的TypeScript特性有的特性之一
- 枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型;
- 枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型;
15.1. 应用
1 | enum Direction{ |
15.2. 枚举类型的值
枚举类型默认是有值的,比如上面的枚举,默认值是这样的
当然,也可以给枚举其他值
- 从100进行递增;
也可以给他们赋值其他的类型:
1 | enum Direction1 { |
16. 泛型
软件工程的主要目的是构建不仅仅明确和一致的API,还要让你的代码具有很强的可重用性:
- 比如可以通过函数来封装一些API,通过传入不同的函数参数,让函数完成不同的操作;
- 但是对于参数的类型是否也可以参数化呢?
什么是类型的参数化?
- 提一个需求:封装一个函数,传入一个参数,并且返回这个参数;
如果是TypeScript的思维方式,要考虑这个参数和返回值的类型需要一致:
1
2
3function foo(arg:number):number{
return arg;
}上面的代码虽然实现了,但是不适用于其他类型,比如string、boolean、Person等类型:
1
2
3function foo(arg:any):any{
return arg;
}
16.1. 泛型实现类型参数化
虽然any是可以的,但是定义为any的时候,其实已经丢失了类型信息
- 比如传入的是一个number,那么希望返回的可不是any类型,而是number类型;
- 所以,需要在函数中可以捕获到参数的类型是number,并且同时使用它来作为返回值的类型;
需要在这里使用一种特性的变量 - 类型变量(type variable),它作用于类型,而不是值
1
2
3function foo<Type>(arg:Type):Type{
return arg;
}可以使用两种方式来调用它
- 方式一:通过 <类型> 的方式将类型传递给函数;
- 方式二:通过类型推到,自动推到出我们传入变量的类型:
- 在这里会推导出它们是字面量类型的,因为字面量类型对于函数也是适用的
1
2foo<string>("abc");
foo<number>(123);
16.2. 泛型的基本补充
- 可以传入多个类型
- 平时在开发中可能会看到一些常用的名称
- T:Type的缩写,类型
- K、V:key和value的缩写,键值对
- E:Element的缩写,元素
- O:Object的缩写,对象
1 | function foo<T,E>(a1:T, a2:E){ |
16.3. 泛型接口
1 | interface Foo<T>{ |
16.4. 泛型类
1 | class Point<T>{ |
16.5. 泛型约束
- 有时候希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中
- 比如string和array都是有length的,或者某些对象也是会有length属性的;
- 那么只要是拥有length的属性都可以作为参数类型,那么应该如何操作呢?
1 | interface ILength{ |
17. 模块化开发
- TypeScript支持两种方式来控制作用域
- 模块化:每个文件可以是一个独立的模块,支持ES Module,也支持CommonJS;
- 命名空间:通过namespace来声明一个命名空间
17.1. 模块化
1 | export function sum(num1:number,num2:number):number{ |
17.2. 命名空间namespace
- 命名空间在TypeScript早期时,称之为
内部模块
- 主要目的是将一个模块内部再进行作用域的划分,防止一些命名冲突的问题
- 在namespace中可以命名变量,函数,但只有声明export后才可以被外部调用
1 | export namespace Time{ |
调用
1 | import { Time,Price } from './math'; |
18. 类型的查找
1 | const imageEl = document.getElementById("image") as HTMLImageElement; |
HTMLImageElement类型来自哪里呢?甚至是document为什么可以有getElementById的方法呢?
- 其实这里就涉及到typescript对类型的管理和查找规则了
另外的一种typescript文件:.d.ts文件
- 之前编写的typescript文件都是 .ts 文件,这些文件最终会输出 .js 文件,也是通常编写代码的地方;
- 还有另外一种文件 .d.ts 文件,它是用来做类型的声明(declare)
- 它仅仅用来做类型检测,告知typescript有哪些类型;
那么typescript会在哪里查找类型声明呢?
- 内置类型声明;
- 外部定义类型声明;
- 自己定义类型声明;
18.1. 内置类型声明
内置类型声明是typescript自带的、内置了JavaScript运行时的一些标准化API的声明文件;
- 包括比如Math、Date等内置类型,也包括DOM API,比如Window、Document等;
内置类型声明通常在安装typescript的环境中会带有的;
18.2. 外部定义类型声明
- 外部类型声明通常是使用一些库(比如第三方库)时,需要的一些类型声明
- 这些库通常有两种类型声明方式
- 方式一:在自己库中进行类型声明(编写.d.ts文件),比如axios
- 方式二:通过社区的一个公有库DefinitelyTyped存放类型声明文件
- 该库的GitHub地址:https://github.com/DefinitelyTyped/DefinitelyTyped/
- 该库查找声明安装方式的地址:https://www.typescriptlang.org/dt/search?search=
- 比如安装react的类型声明:npm i @types/react –save-dev
18.3. 自定义声明
- 什么情况下需要自己来定义声明文件呢?
- 情况一:使用的第三方库是一个纯的JavaScript库,没有对应的声明文件;比如lodash
- 情况二:代码中声明一些类型,方便在其他地方直接进行使用;
18.4. 声明变量-函数-类
1 | declare let tName: string; |
18.5. 声明模块
也可以声明模块,比如lodash模块默认不能使用的情况,可以自己来声明这个模块
1
2
3declare module 'lodash'{
export function join(arr:any[]):void
}声明模块的语法:declare module ‘模块名’ {}
- 在声明模块的内部,可以通过 export 导出对应库的类、函数等;
18.6. declare文件
- 在某些情况下,也可以声明文件
- 比如在开发vue的过程中,默认是不识别 .vue文件的,那么就需要对其进行文件的声明;
- 比如在开发中使用了 jpg 这类图片文件,默认typescript也是不支持的,也需要对其进行声明;
1 | declare module '*.vue'{ |
18.7. declare命名空间
在index.html中直接引入了jQuery
可以进行命名空间的声明
1
2
3declare namespace ${
function ajax(settings:any):void
}在main.ts中就可以使用了
1
2
3
4
5
6$.ajax({
url: 'http://123.207.32.32:8000/home/multidata',
success: (res:any)=>{
console.log(res);
}
})
19. tsconfig.json文件
- tsconfig.json是用于配置TypeScript编译时的配置选项 https://www.typescriptlang.org/tsconfig
1 | { |