TypeScript 概述
概念
TypeScript是具有类型语法的javascript,是一门强类型的编程语言。
TypeScript是微软开发的开源编程语言,Type
+ JavaScript(type是类型 => 在 JS 基础之上,为 JS 添加了类型支持),简称:TS。
代码层面
// TS 代码
// 变量age1是强类型的,有明确的类型。即: number(数值类型)
let age1: number = 18
age1 = '18' // 报错
// --------------------
// javascript代码
// 变量age2是弱类型的 无明确的类型
let age2 = 18
age2 = '18' // 不报错
带来的好处
- 静态类型检查,提前发现代码错误
2. 良好的代码提示,提升开发效率
什么情况用TS而不是js
以下是一些建议
- 你做的是一个大型的项目?
- 是否是一个团队开发模式?
- 是否在编写通用的代码库?
结论:
ts不是万能的,技术的选项不能脱离具体的业务和应用场景,TS更加适合开发中大型的项目,或者是通用的JS代码库,再或者是团队协作开发的场景
TS学什么
- 不学:变量,循环,函数 ....
- 要学:
-
- 类型
- 与具体前端开发框架(vue, react)的结合使用
TS怎么学
- TS核心 + 案例
- V3 + ts + 实战案例
参考
TypeScript: 演练场 - 一个用于 TypeScript 和 JavaScript 的在线编辑器
小结
- typescript是________ 公司开源的编程语言, type是 ________ 的意思,表示在js的基础之上提供了______ 支持。
运行第一个TS代码
安装编译 TS 的工具包
TypeScript编写的代码是无法直接在js引擎(浏览器/Nodejs)中运行的,最终还需要经过编译成js代码才可以正常运行。
带来的好处: 既可以在开发时使用TS编写代码享受类型带来的好处,同时保证运行的还是JS代码。
搭建手动编译环境
- 全局安装 typescript 包(编译引擎)-> 注册 tsc
命令npm install -g typescript
- 新增 hello.ts 文件, 执行
tsc hello.ts
命令生成hello.js文件 - 执行 node hello.js 运行js文件查看效果
搭建自动编译环境
基于工程化的TS开发模式(webpack / vite),TS的编译环境已经内置了,无需手动安装配置,通过以下命令即可创建一个最基础的自动化的TS编译环境
npm create vite@latest ts-pro -- --template vanilla-ts
在线体验版本:Vitejs - Vite (forked) - StackBlitz
命令说明:
- npm create vite@latest 使用最新版本的vite创建项目
- ts-pro 项目名称
- -- --template vanilla-ts 创建项目使用的模板为原生ts模板
拓展: 运行命令的来源
社区模板:
开始 | Vite 官方中文文档
开始 | Vite 官方中文文档
GitHub - vitejs/awesome-vite: ⚡️ A curated list of awesome things related to Vite.js
小结
1. 浏览器中可以直接使用ts代码吗?
不可以,需要先编程为js才能执行
2. 哪个包可以负责把ts代码编译为js代码?
typescript
3. 实际工作中需要我们手动去把ts编译成js代码吗?
不需要,由工程化内置,自动编译
类型注解
概念:类型注解指的是给变量添加类型约束,它的好处是:
- 使变量只能被赋值为约定好的类型
- 编写代码的过程中可以有相关的类型提示
示例代码:
let age: number = 18
说明:代码中的 : number
就是类型注解
作用:为变量添加类型约束。比如,上述代码中,约定变量 age 的类型为 number 类型
解释:约定了什么类型,就只能给变量赋值该类型的值,否则,就会报错
约定了类型之后,代码的提示就会非常的清晰
TS支持的常用类型注解
- JS 已有类型
-
- 原始类型,简单类型(
number/string/boolean/null/undefined
) - 复杂数据类型(数组,对象,函数等)
- 原始类型,简单类型(
- TS 新增类型
-
- 联合类型
- 自定义类型(类型别名)
- 接口
- 元组
- 字面量类型
- 枚举
- void
- ...
简单类型如何进行类型注解
简单类型的注解完全按照 JS的类型(全小写的格式)来书写即可
小结
1. 类型注解的作用是?
限制变量赋值的数据类型 并 给出提示
2. 类型注解的语法是?
变量:类型
类型推论
问:每个简单的变量都要写类型注解么?
let age: number = 18
let myName: string = '小花'
let isLoading: boolean = false
上面的类型注解,是不是都点多余了。
类型推论
某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型
换句话说:由于类型推论的存在,有些场合下的类型注解可以省略不写
发生类型推论的 2 种常见场景:
- 声明变量并初始化时
- 决定函数返回值时
示例代码
// 变量 age 的类型被自动推断为:number
let age = 18
// 函数返回值的类型被自动推断为:number
function add(num1: number, num2: number) {
return num1 + num2
}
小结
1. 什么是类型推论?
ts会自动去判断变量的类型,这样我们就可以减少一些类型注解了。
2. 类型推论的两种场景?
初始化变量,函数返回值
技巧
- 能省略类型注解的地方就省略(偷懒,充分利用TS类型推论的能力,提升开发效率)
- 如果不知道类型,可以通过鼠标放在变量名称上,利用 VSCode 的提示来查看类型
数组类型注解
注解数组有什么用
变量被注解为数组类型之后,有两点好处:
- 不仅可以限制变量类型为数组而且可以限制数组成员的类型
- 编码时不仅可以提示数组的属性和方法而且可以提示成员的属性和方法
如何注解数组类型
使用数据类型对变量进行类型注解有两种语法
语法1:
上面的代码表示: 变量arr只能用来保存数组类型,并且数组的元素必须是number类型
语法2:泛型函数
小结
1. 数组类型注解注解之后除了限制数组类型还限制了什么?
对数组中每一个成员的类型有限制
2. 实际开发时常用的是哪种数组注解方式?
:类型[]
思考题
有一个变量arr, 要求用两种方式添加类型注解,使其只能赋值成员都是字符串的数组?
联合类型
需求
数组中既有 number 类型,又有 string 类型,这个数组的类型应该如何写?
let arr: (number | string)[] = [1, 'a', 3, 'b']
联合类型
概念:将多个类型合并为一个类型对变量进行注解
解释:|
(竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种
注意:这是 TS 中联合类型的语法,只有一根竖线,不要与 JS 中的或(|| 或)混淆了
小结
1. 联合类型的作用是什么?
把多个类型合并为一个类型
2. 联合类型的格式?
多个类型用 | 隔开。
思考题
有一个变量timerId, 它的类型可能是null也可能是number。如何定义?
类型别名-type
目标
掌握类型别名的用法,能给类型起别名
思考
const arr1:(number | string)[] = [1, '1']
const arr2:(number | string)[] = ['a', 'b']
如上代码中,存在类型注解重复的问题: (number | string)[]
这个类型出现了多次, 怎么优化?
类型别名
定义: type 别名 = 类型
别名可以是任意的合法字符串,一般首字母大写
例子: type MyArr = (number | string) []
作用:给类型起别名 ---> 定义了新类型
使用:
type MyArr = (number | string) []
const arr1:MyArr = [1, '1']
const arr2:MyArr = ['a', 'b']
图示
小结
1. 定义类型别名的关键字是? 别名的取名有什么要求?
type。 首字符大写
2. 类型别名有什么好处?
简化和复用类型
函数类型
1. 基础使用
概念:函数类型是指给函数添加类型注解,本质上就是给函数的参数和返回值添加类型约束
说明:
- 函数参数注解类型之后不但限制了参数的类型还限制了参数为必填
- 函数返回值注解类型之后限制了该函数内部return出去的值必须满足类型要求
好处:
- 避免因为参数不对导致的函数内部逻辑错误
- 对函数起到说明的作用
2. 函数表达式
函数表达式的类型注解有两种方式:
- 参数和返回值分开注解
- 函数整体注解
参数和返回值分开注解
函数整体注解(只针对于函数表达式)
函数可选参数
概念:可选参数表示当前参数可传可不传,一旦传递实参必须保证参数类型正确
说明:lastName参数表示可选参数,可传可不传,一旦传递实参必须保证类型为string类型
函数无返回值 - void
概念:JS中的有些函数只有功能没有返回值,此时使用void进行返回值注解,明确表示函数没有函数值
注意事项:
- 在JS中如果没有返回值,默认返回的是undefined
- 在TS中 void和undefined不是一回事:
-
- undefined在TS中是一种明确的简单类型,如果指定返回值为undefined,那返回值必须是undefined类型
- void表示没有返回值
小结
1. 函数类型实际上是给谁标注类型?
参数和返回值
2. 可选参数可以不放在参数列表的末尾吗?
不可以,必须在所有的参数末尾
3. 函数返回值值为void和undefined类型是一回事吗?
不是,void代表没有返回值,undefined在TS中是一种具体的类型
思考题
编写一个arr2Str函数,作用为把数组转换为字符串,其中数组中既可以包含字符串和数字,分隔符也可以进行自定义,类型为字符串类型,使用样例:
// 在下面js代码的基础上,对arr2Str进行ts改写
const arr2Str = (arr, split) => {
return arr.join(split)
}
console.log(arr2Str([1,2,3], '-')) // ===> 1&2&3
console.log(arr2Str(['a','b',3], '&')) // ===> a&b&3
console.log(arr2Str(['a','b',false], 0)) // 报错
interface-接口-基本使用
作用
作用: 用interface来描述对象数据的类型(常用于给对象的属性和方法添加类型约束)
说明:一旦注解接口类型之后对象的属性和方法类型都需要满足要求,属性不能多也不能少
典型场景
场景:在常规业务开发中比较典型的就是前后端数据通信的场景
- 前端向后端发送数据:收集表单对象数据时的类型校验
- 前端使用后端数据:渲染后端对象数组列表时的智能提示
小结
1. interface 的作用是?
用来描述对象类型的格式
2. interface的使用格式是
interface 接口名 { 属性名: 类型}
练习
如下,尝试使用interface接口定义其类型
interface news {
// 补齐你的代码
}
const news1: news = {
code: 200,
msg: 'success',
data: {
title: '当我想明白这件事之后',
content: '20年前,少年误入歧途....',
}
}
interface接口的可选设置和继承
场景
对象的某个属性是可选的。
例如:从后端取回的商品数据中,一定有id,name, price,但是imgUrl是可选的。表示有些商品没有配图片。
const goodList = [
{id:1, name: '手机', price: 2999, imgUrl: 'http://w.g1.png'},
{id:1, name: '毛巾', price: 9}
]
我们这么约定goodList的格式,下面的代码会报错:
interface goodItem {
id: number;
name: string;
price: number;
imgUrl: string;
}
const goodList: goodItem[] = [
{id:1, name: '手机', price: 2999, imgUrl: 'http://w.g1.png'},
{id:1, name: '毛巾', price: 9} // 这里会报错
]
可选设置
概念: 通过 ?对属性进行可选标注,赋值的时候该属性可以缺失,如果有值必须保证类型满足要求。
格式:
interface 接口名{
属性1:类型1,
属性2?:类型2, // 属性2是可选的
属性3?:类型3, // 属性3是可选的
}
代码
interface goodItem {
id: number;
name: string;
price: number;
imgUrl?: string; // 可选的
}
const goodList: goodItem[] = [
{id:1, name: '手机', price: 2999, imgUrl: 'http://w.g1.png'},
{id:1, name: '毛巾', price: 9} // 这里也是正确的
]
接口的继承
概念:接口的很多属性是可以进行类型复用的,使用 extends 实现接口继承,实现类型复用。
// 正常的商品
interface goodItem {
id: number;
name: string;
price: number;
imgUrl?: string;
}
// 打折的商品:正常商品 + newPrice + effectDate
interface goodItemDiscount {
id: number;
name: string;
price: number;
imgUrl?: string;
newPrice: number;
effectDate: Date
}
继承之后
interface goodItemDiscount extends goodItem {
newPrice: number;
effectDate: Date
}
小结
1. interface 如何设置可选属性?
{属性?: 类型}
2. 接口继承的关键字是?
extends
3. 继承解决了什么问题?
类型的复用问题
type注解对象类型
注解对象
概念:在TS中对于对象数据的类型注解,除了使用interface之外还可以使用类型别名来进行注解,作用相似
type + 交叉类型模拟继承
类型别名配合交叉类型(&)可以模拟继承,同样可以实现类型复用
interface 对比 type
相同点
- 都能描述对象类型
- 都能实现继承,interface使用extends, type配合交叉类型
不同点
- type除了能描述对象还可以用来自定义其他类型
- 同名的interface会合并(属性取并集,不能出现类型冲突)
- 同名type会报错
在注解对象类型的场景下非常相似,推荐大家使用type, type更加灵活
练习
改造interface为type
// 正常的商品
interface goodItem {
id: number;
name: string;
price: number;
imgUrl?: string;
}
// 打折的商品:正常商品 + newPrice + effectDate
interface goodItemDiscount {
id: number;
name: string;
price: number;
imgUrl?: string;
newPrice: number;
effectDate: Date
}
字面量类型
思考
以下代码,两个变量的类型分别是什么?
let str1 = 'abc'
const str2 = 'abc'
通过 TS 类型推论机制,可以得到答案:
- 变量 str1 的类型为:string
- 变量 str2 的类型为:'abc'
解释:
- str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string
- str2 是一个常量(const),它的值不能变化只能是 'abc',所以,它的类型为:'abc'
注意:此处的 'abc',就是一个字面量类型,也就是说某个特定的字符串也可以作为 TS 中的类型,任意的 JS 字面量(比如,对象、数字等)都可以作为类型使用
字面量类型的实际应用
字面量类型在实际应用中通常和联合类型结合起来使用,提供一个精确的可选范围
场景1:性别只能是 ’男‘ 和 ’女‘,就可以采用联合类型配合字面量的类型定义方案
场景2:ElementUI中的el-button组件按钮的type属性
any 类型
当值的类型为 any 时,可以对该值进行任意操作,并且不会有代码提示
let obj: any = { x: 0 } // any 就是任意类型
obj.bar = 100
obj()
const n: number = obj
解释:
- 以上操作都不会有任何类型错误提示,即使可能存在错误
两种隐式 any 类型的情况
- 声明变量不提供类型也不提供默认值
- 函数参数不加类型
原则:
- 不推荐使用 any! 这会让 TypeScript 变为 “AnyScript”(失去 TS 类型保护的优势)
- 除非临时使用 any 来“避免”书写很长、很复杂的类型
类型断言
问题
获取页面中的id为link的a元素,尝试通过点语法访问href属性
发现代码报错了
原因是:getElementById方法返回值的类型是 HTMLElement,而这个类型只包含所有标签公共的属性或方法,这个类型太宽泛(不具体),不包含 a 标签特有的 href 等属性。
类型断言
有时候你会比 TS 更加明确一个值的类型,此时,可以使用类型断言(as)来指定更具体的类型
应用场景:把一个大类型 缩小 为更加具体的类型
格式:类型 as 具体类型
使用类型断言:
解释:
- 使用
as
关键字实现类型断言 - 关键字 as 后面的类型是一个更加具体的类型(HTMLAnchorElement 是 HTMLElement 的子类型)
- 通过类型断言,aLink 的类型变得更加具体,这样就可以访问 a 标签特有的属性或方法了
小结
类型断言的场景是?
把一个宽泛的类型变得具体
类型断言的格式是?
as 新类型
练习
在发请求拿数据的过程中有一种典型的场景:我们提前知道了后端返回的数据的类型,也定义了个变量用来保存数据,把他的初值为{}。但是,下面的代码会报错
怎么解决?
type Res = {
total: number,
list: object[]
}
// 我们确定当ajax数据回来之后,result的数据就是上面的Res格式
// 但是,它的初值为 {}
const result:Res = {}
泛型
思考:后端接口给前端返回数据时,一般会遵循一定的规律,例如:都有code, msg, data三个属性,而不同的接口返回的具体数据保存在data中,它的格式是不同的。
如下是两个典型的返回结果:
// 请求得到用户信息
const res1 = {code: 200, msg: 'success', data: {name: 'jack', age: 18}}
// 请求得到商品信息
const res2 = {code: 200, msg: 'success', data: [{id: 1001, goodsName: '衬衣'}}]
提问:如何给他们提供注解?
示例代码: 定义两个不同的接口
interface Res1 {
code: number,
msg: string,
data: {
name: string,
age: number
}
}
interface Res2 {
code: number,
msg: string,
data: {
goodsName: string,
id: number
}[]
}
const res1: Res1 = {}//
const res2: Res2 = {}//
问题:两种类型有重复的代码。
概念
泛型(Generics)是指在定义接口、函数等类型的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性, 使用泛型可以复用类型并且让类型更加灵活。
泛型接口
语法:在接口类型的名称后面使用<T>
即可声明一个泛型参数列表,接口里的其他成员都能使用该参数的类型。
类比:函数的形参
通用思路:
- 找到可变的类型部分通过泛型抽象为泛型参数(定义参数)
泛型参数相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)
- 在使用泛型的时候,把具体类型传入到泛型参数位置 (传参)
拓展
TS中,对js的数组做的处理:
泛型-类型别名
语法:在类型别名type的后面使用<T>
即可声明一个泛型参数,接口里的其他成员都能使用该参数的类型。
需求:使用泛型别名重构ResData案例
const res1 = {code: 200, msg: 'success', data: {name: 'jack', age: 18}}
const res2 = {code: 200, msg: 'success', data: [{id: 1001, goodsName: '衬衣'}}]
参考
type User = {name: string, age: number}
type Good = {id:number, goodsName: string}
type Res<T> = {
code: number
msg: string
data: T
}
type UserInfo = Res<User>
type GoodInfo = Res< Good[] >
const res1:UserInfo = {code: 200, msg: 'success', data: {name: 'jack', age: 18}}
const res2:GoodInfo = {code: 200, msg: 'success', data: [{id: 1001, goodsName: '衬衣'}] }
console.log(res1.data.)
// res1
// res1.data.age
export {}
泛型-函数
需求:定义一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值(多种类型)
function createArray(len, initValue) {
let result = [];
for (let i = 0; i < len; i++) {
result[i] = initValue
}
return
}
createArray(4,'a') // 得到一个长度为4的数组,它的每个元素都是'a'
createArray(3,1) // 得到一个长度为3的数组,它的每个元素都是1
语法:在函数名称的后面使用<T>即可声明泛型参数,整个函数中(参数、返回值、函数体)的变量都可以使用该参数的类型
参考代码
function createArray<T>(len: number, initValue:T) : T[] {
let result: T[] = [];
for (let i = 0; i < len; i++) {
result[i] = initValue
}
return result
}
泛型约束
作用:泛型的特点就是灵活不确定,有些时候泛型函数的内部需要访问一些特定类型的数据才有的属性,此时会有类型错误,需要通过泛型约束解决
下面是一个报错的例子:
泛型约束的格式
<T extends 具体类型>
添加约束
综合案例
目标
使用 TS 实现访问历史记录功能,刷新页面后,展示访问历史记录,记录包含:次数和时间。
核心思路
- 从本地获取到当前最新列表,取出当前列表中的最后一条记录
- 在最后一条记录的基础上把次数加一,重新把次数和当前时间添加到列表的尾部
- 把最新列表渲染到页面
- 把最新列表再次存入本地
步骤
src/main.ts
- 封装格式化时间函数,支持 Date 和 string 格式的时间,可选参数,转换成功 10:10:10 时分秒。
const formatTime = (date?: Date | string): string => {
// #1.1 如果没有传递参数,就是当前时间
if (!date) date = new Date()
// #1.2 如果传递的是字符串,把字符串转换为日期对象
if (typeof date === 'string') date = new Date(date)
// #1.3 获取时、分、秒并返回
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();
return `${h}:${m}:${s}`;
};
- 定义本地数据的类型别名 List
// 注意:这儿是自己定义的类型 Data,而不是日期对象类型 Date
type Data = {
count: number;
time: string;
};
type List = Array<Data>
- 封装获取本地数据的方法 getData
const KEY = "ts-demo-data";
const getData = () => {
const str = localStorage.getItem(KEY);
return JSON.parse(str || "[]") as List;
};
- 封装存储数据到本地的方法 saveDataToLocal
const saveDataToLocal = () => {
// 从本地获取旧数据
const list = getData()
// 拿到旧数据的最后一条
const lastItem = list[list.length - 1];
list.push({
// 在最后一条 count 的基础上 +1
count: lastItem ? lastItem.count + 1 : 1,
time: formatTime(),
});
// 存储到本地
localStorage.setItem(KEY, JSON.stringify(list));
};
- 封装渲染历史记录的 render 方法并调用。
const render = () => {
// 先往本地存一条数据
saveDataToLocal();
// 获取数据
const data = getData()
const app = document.querySelector("#app") as HTMLDivElement;
app.innerHTML = data
.map((item) => `次数:${item.count},时间:${item.time}`)
.join("<br/>");
};
render();