01、什么是Typescript、ts
ts: TypeScript 的缩写,是微软开发的编程语言, Type+JavaScript (Type是类型 ===》在JS基础之上,为了JS添加了支持类型)
那我们为什么要学习ts呢?js不香嘛? 我们来看这一段js代码
let age = 18
age = '19' //正确
在js定义变量中,定义了一个age的变量,然后再去修改它的值,我们只希望赋值是一个数字类型代表年龄,并不希望是一个字符串或其他类型。
而在ts中就可以避开这个错误
let age:number = 18
如果这时你去赋值一个字符串类型的19,那么编辑器会直接告诉你 不能把string的类型分配给类型number
再来吃个栗子
var num = 18
num.toLowerCase()
在js中这两行代码并没有任何问题,但是我们知道 数字类型上是没有toLowerCase这个方法的,如果换到ts再来看
发现在编辑器时已经显示红色波浪线了,把鼠标放到波浪线也会有原因告知你, 所以这也是为什么要学习ts的主要原因
背景:js的类型系统中存在 “先天缺陷”弱类型,JS代码中绝大部分错误都是类型错误 类型错误的例子
经常出现的错误 导致了在js进行项目开发时 增加了找BUG和改BUG 的时间
对于JS来说:需要等到代码真正执行的时候才能发现错误(晚)
对于TS来说:在代码编译的时候(代码执行前)就可以发现错误(早)
并且,配合 VSCode 等开发工具,TS 可以提前到在编译写代码的同时就发现代码中的错误减少找BUG、改BUG时间
02、使用Typescript
在平常使用一些ui库、框架时都会发现会有后缀名是.ts文件的,但是ts在浏览器或者是服务器是无法直接识别的,所有我们就需要安装编译TS的工具包转成JS
npm i -g typescript
或
yarn global add typescript
安装好之后我们就可以新建ts文件 去编译啦
1. 创建 hello.ts 文件(注意:TS 文件的后缀名为 .ts)
2. 将 TS 编译为 JS:在终端中输入命令,tsc hello.ts(此时,在同级目录中会出现一个同名的 JS 文件)
3. 执行 JS 代码:
运行:
a. 在终端中输入命令,node hello.js
b. 把hello.js引入某个html文件来运行
注意:
真正在开发过程中,其实不需要自己手动的通过tsc把ts文件转成js文件,这些工作应该交给webpack或者vite来完成
03、定义变量
一、原始类型
let age:number = 18
let myName:string = '奥特曼'
let flag: Boolean = false
只需要在变量名后定义 :类型 即可
编译成的js发现并没有任何区别,但书写ts文件时 帮助还是很大的
var age = 18;
var myName = '奥特曼';
var flag = false;
二、类型推断
我们每定义一个变量发现都要在后面写一下类型,这点还是比较繁琐的。
在TS中,某些没有明确指出类型的地方,TS的类型推论机制会帮助提供类型 换句话说 由于类型推论的存在 有些场合下的类型注解可以省略不写
发送类型推论的 两种常见常见:
1.声明变量并初始化时
2.决定函数返回值时
1.基本类型
let age5 = 15
如果你不写类型,那么TS的类型推断机制会帮助你推断出定义的类型
如果你想要的修改 发现赋值一个字符串的类型也是不允许的
2.数组的类型推断
数字类型
let list = [1, 2, 3, 4]
会自动推断成,当然如果你直接定义时 直接写成下面这样也是一样的
let list:number[] = [1, 2, 3, 4]
多个类型
let list = [1, 2, 3, 4,'5']
会自动推断成
let list:(string|number)[] = [1, 2, 3, 4,'5']
第二种写法(扩展)
let arr2: Array<string> = ['1', '2']
let arr2: Array<string|number> = ['1', '2', 3]
3.对象的推断
let user = {name:'奥特曼',age:18}
会自动推断成
let user: { name: string; age:number} = {name:'奥特曼',age:18}
此时已经限定了name就是字符串类型,age就是数值类型,如果你想赋值成不同的类型会报错
let user1 = { name: '奥特曼', age: 18,lesson:[{title:'vue'},{title:'react'}] }
会自动推断成
let user1: {
name: string;
age: number;
lesson: {
title: string;
}[];
} = { name: '奥特曼', age: 18,lesson:[{title:'vue'},{title:'react'}] }
在上面的lesson中只允许每个对象中有一个title属性,不允许有其他属性存在
三、联合类型
能够通过联合类型将多个类型组合成一个类型
例 一个变量即可以有数值类型也可以有字符串类型
let f: string | number = 1
f = '学习ts'
例:数组中既有number类型 又有string类型
let arr3: (number | string)[] = [2, 3, 'a']
let arr2: Array<number|string> = ['1', '2', 3]
四、any类型
any意为任何(什么类型都可以用any定义),如果你使用了any 等同于使用了js,会让Typescript 变成 “AnyScript” (失去TS类型保护的优势)
因此当值为any时,可以对值改进行任意的操作,并不会有代码提示(失去了严格模式,所以我们也要少用any)
let obj: any = { x: 0 }
obj.bar = 100
obj()
obj = undefined
obj= null
const n:number = obj
以上操作都不会有任何类型的错误提示,即使可能存在错误
尽可能的避免any类型 除非临时使用any来避免 书写很长、很复杂的类型
其他隐式具有any类型的情况
声明变量不提供类型也不提供默认值
函数参数不加类型
注意:因为不推荐使用any 所以这两种情况应该提供类型
五、unknown和as
unknown意为未知的,表示我不知道你是什么类型
如果你把一个unknow类型赋值给一个字符串 他就会报出提示
如果你想要赋值
let userName:unknown = '奥特曼'
let uName: string = userName as string
as为类型断言,通过类型断言 可以让userName变得更加具体 就代表userName是属于string类型的
但是要注意
字符串如果想直接转成数字类型是不允许的,如果你真的想转换可以先转为unknow 在转换为number
let aa:string = '15'
let bb:number = aa as unknown as number
六、void和undefined
void意为空的,如果函数没有返回值,那么函数的返回值类型为void
function logUserName(): void {
console.log('我是奥特曼');
}
如果一个函数没有返回值,此时 在TS的类型中 应该使用void类型 有如下三种情况会满足
1.不写return
2.写return 但是后面不接内容
3.写 return undefined
此时如果什么都不写 add的返回值类型为void
const add22 = () => { }
但如果指定了返回值是undefined,此时,函数体中显示return undefined
const add24 = (): undefined => { return undefined }
七、类型别名
格式:
type 别名 = 类型
type s = string // 定义
const str1:s = 'abc'
const str2: string = 'abc'
作用
1.给类型起别名
2.定义了新类型
场景1:给基础数据类型起别名
type newType = string | number
let a:newType = 5
let b:newType = '奥特曼'
场景2:给复杂数据类型起别名
type TypeArr = (number | string)[]
const arr6: TypeArr = [1, 2, 3, 'a', 'b']
type Age = {
age:number
}
type Name = {
name:string
}
// 合并使用
type Users = Age & Name
let userInfo: Users = {
age: 10,
name:'奥特曼'
}
八、函数
函数类型实际上指的是,函数的形参和返回值类型
普通函数: function 函数名(形参1:类型=默认值,形参2:类型=默认值):返回值类型{ }
function sum(num1:number=99,num2:number=88):number {
return num1+num2
}
箭头函数: const 函数名(形参1:类型=默认值,形参2:类型=默认值):返回值类型=>{ }
const sum12=(num1:number=99,num2:number=88):number =>{
return num1+num2
}
注意:函数的返回值的类型注解写在( )的后边
可选参数
格式:在可选参数的后面添加? (问号)
function addNum(num1: number, num2: number, num3?: number): number {
return (num1 * num2) * (num3 ? num3 : 1)
}
console.log(addNum(1, 2, 3));
注意:可选参数只能出现在参数列表中的最后也就是说可选参数后面不能再出现必选参数
可选参数和默认值的区别
相同点:调用函数时,可以少传参数
区别:设置了默认值之后,就是可选的了,不写就会使用默认值,可选的并非一定有值
注意:他们不能一起使用。优先使用默认值
function nameSlice2(name?:string='迪迦',start?:number,end?:number):void { // 错误语法
console.log(name,start,end);
}
函数的结构定义
type numList = {num1:number,num2:number}
type addFn = ( num:numList ) => number
let addNums:addFn = (num:numList):number =>{
return num.num1 + num.num2
}
console.log(addNums({ num1:1, num2:2}));
剩余参数
格式: 写在末尾用...展开:类型 [ ]
例:求和
const reduce = (num1: number, num2: number, ...num3: number[]): number => {
return num1 + num2 + num3.reduce((a,i)=>a+i)
}
console.log(reduce(1, 2, 3, 4, 5));
例:合并数组
const mergeArr = (arr:string[],...args:string[]):any[] => {
arr.push(...args)
return arr
}
console.log(mergeArr(['迪迦','雷欧'],'泰罗','赛文'));
注意:
剩余参数 只能 有一个
剩余参数 只能 定义为数组
剩余参数 只能 定义在列表中的最后
九、对象
JS中对象是由属性和方法构成的,而TS对象的类型就是在表述对象的结构(有什么属性方法)
空对象
let person: {} = {}
有属性的对象
let person1: { name: string } = {
name:'同学'
}
既有属性又有方法的对象
let person2: { name: string; say(): void } = {
name: '奥特曼',
say:()=>{}
}
对象中如果有多个类型,可以换行写 ;可以省略
let person3: {
name: string
say(): void
} = {
name: '奥特曼',
say:()=>{}
}
1. 使用{} 来描述对象结构
2.属性采用 属性名:类型的形式
3.方法采用 方法名(): 返回值类型的形式
使用类型别名
注意 直接使用{}形式为对象添加属性,会降低代码 的可读性(不好辨别类型和值)
推荐:使用类型别名为对象添加类型
对象中箭头函数形式的方法类型
语法
{
greet(name: string):string,
greet: (name: string) => string
}
示例
type Person1 = {
sayHi: (hi: string) => void;
sayHello(hello:string):void
}
let person6:Person1 = {
sayHi(hi) {
console.log(hi);
},
sayHello(hello) {
console.log(hello);
}
}
对象中的可选属性
对象的属性和方法也是可选的,此时就用到了可选属性,比如 我们在使用axios({...})时,如果发送GET请求,methods属性就可以省略
可选属性的语法与函数的可选参数语法 都使用 ? 表示
type Config = {
url: string
method?:string
}
function myAxios(config: Config) {
}
myAxios({url:'xxx'})
十、元组
场景:在某些地图场景中,使用经纬度来标记位置信息
可以用数组来记录坐标,那么,该数组中只有两个元素,并且这两个元素都是数值类型
let position: number[] = [116.2317, 39.5427]
使用number[]的缺点:不严谨 因为该类型的数组中可以出现任意多个数字
元组类型是另一种类型的数组,它确切的知道包含多少个元素,以及特定索引对应的类型
let position:[number,number] = [39.54454,116.2315]
1. 元组类型可以确切地标记出有多少个元素, 以及每个元素的类型
2. 该示例中,元素有两个元素 每个元素都是number
十一、枚举
枚举:定义一组命名常量,它描述一个值,该值可以是这些名常量中的一个
enum Directions { up, Down, Left, Right }
function changeDirections(direction: Directions) {
console.log(direction)
}
changeDirections(Directions.up)
调用函数时,需要应该传入: 枚举Directions成员的任意一个,类似于JS中的对象 直接通过点(.)语法 访问枚举的成员
a.使用enum关键字定义枚举
b.约束枚举名称一大写字母开头
c.枚举中的多个值之间通过,(逗号)分隔
d.定义好枚举后,直接使用枚举名称作为类型注解
数字枚举
问题:我们把枚举成员作为了函数的实参,它的值是什么呢? 通过将鼠标移入Direction.up 可以看到枚举成员Up的值是0
注意:枚举成员是有值的,默认从0开始自增的值
我们把,枚举成员的值为数字的枚举,称为:数字枚举
当然,也可以给枚举中的成员初始化值
字符串枚举
字符串枚举:枚举成员的值是字符串
注意:字符串枚举没有自增长行为,因此 ,字符串枚举的每个成员必须有值
enum Direction3 {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
会被编译成以下 JS代码
(function (Direction) {
Direction['Up'] = 'UP'
Direction['Down'] = 'DOWN'
Direction['Left'] = 'LEFT'
Direction['Right'] = 'RIGHT'
})(Direction || Direction = {})
说明:枚举与前面将的字面量类型+联合类型组合的功能类似,都用来表示一组明确的可选值列表
一般情况下,推荐使用字面量类型 + 联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效
十二、as断言
断言:能够明确的知道结果是什么/类型
function breakfast(falg: boolean): string | number {
return falg? '吃了': 2
}
let result = breakfast(true) as string //断定函数的返回值就是字符串
result = '确实吃了' // 如果赋值为数字会报错
console.log(result);
在上面代码中很明确返回的是字符串 所以用了as
as const
当鼠标移入num很明确 类型为number
let num = 899
num = 10
指定了num1的类型就是99
let num1: 99 = 99
num1 = 99
用const定义了变量就不能再修改或赋值了
const num2 = 98
我们定义的let 也可以断言为const
let num3 = 97 as const
等同于 类型为97
let num3: 97
数组中使用 as count
let a = 1 as const
let b = '奥特曼'
let assertionArray = [a, b]
// 相当于 let assertionArray: (string | 1)[]
注意:a 断言为 const 如果给a赋值为处1其他以外 的值会报错
let assertionArray1 = [a, b] as const
//等同于 let assertionArray1: readonly readonly [1, string]
注意:如果整个数组使用了const 会直接把整个数组变成只读的元组
也可以写成另一种写法
let assertionArray2 = <const>[a, b]
对象中 使用 as count
let a = 1 as const
let b = '奥特曼'
let assertionObj1 = {
a,b
}
只允许修改对象里面的b属性 a是只读的
let assertionObj = {
a:1,
b:2
} as const
如果整个对象使用了const会把所有属性变成只读
非空断言
如果不使用断言 那么ts会推断出 获取的可能是element 也有可能不存在是null
不去使用断言 你会发现 使用dom1. 是没有任何提示的包括会报错,原因就是考虑到了是null的情况 null就不可以在.下去了
let dom1 = document.querySelector('.names') // let dom1: Element | null
断言明确拿到的是div
let ele: HTMLDivElement = document.querySelector('.names') as HTMLDivElement
也可以用!表示非空断言 明确这个值是有的
let ele2: HTMLDivElement = document.querySelector('.names')!
构造函数强制断言
class logEl {
el:HTMLDivElement
constructor(el:HTMLDivElement) {
this.el = el
}
log() {
return this.el.innerHTML
}
}
let div = document.querySelector('.logEl') as HTMLDivElement
let obj = new logEl(div) //若不使用断言 class构造函数会报出 不能接收null
console.log(obj.log());
十三、interface
当一个对象类型被使用多次时,一般会使用interface 来描述对象的类型,达到复用的目的
1.使用interface 关键字声明接口
2.接口名称(比如,此处的IPerson)可以是任意合法的变量名称 推荐以I开头
3.声明接口后,直接使用接口名称作为变量的类型
因为每一行只有一个属性类型,因此,属性类型后面没有;
interface IPerson {
name: string
age: number
sayHi():void
}
let person112:IPerson = {
name: '哦哦',
age: 18,
sayHi(){}
}
interface vs type
interface(接口)和type(类型别名)的对比
相同点:都可以给对象指定类型
不同点:1. 接口,只能给对象指定类型 2.类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名
推荐 能使用type就使用type
接口继承
如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来, 通过即从来实现复用
比如,这两个接口都有x,y两个属性,可重复写两次,可以 但是很繁琐
interface IPoint2D {x:number,y:number}
interface IPoint3D {x:number,y:number,z:number}
更好的实现方式
interface IPoint4D { x: number, y: number }
interface IPoint5D extends IPoint4D {
z:number
}
a.使用extends 继承关键字实现了接口IPoint3D继承IPoint2D
十四、泛型
创建一个函数 我们希望想要得到的返回值 是传入的类型 例如下面代码返回值 一定是number
function itself(arg:number):number {
return arg
}
const res = itself(1)
为了能让函数接收任意类型,如果使用了any 那么就失去了TS的保护能力
function itself(arg:any):any {
return arg
}
const res = itself('奥特曼')
泛型函数
[泛型],顾名思义,宽泛的类型,就是类型不是固定的,不是写死的;不是任意类型
定义格式
function 函数名<类型变量1,类型变量2,...>(参数1:类型1,参数2:类型2):返回值类型{
}
// 调用格式
const 返回值 = 泛型函数名<类型1,类型2,...>(实参1,实参2,实参3,...)
可以适用于多个类型,使用类型变量(比如T)帮助我们捕获传入的类型,之后我们就可以继续使用这个类型
本质是参数化类型,通俗的将,就是所操作的数据被指定为一个参数,这种参数类型可以用在类、接口和函数的创建中,分别称为泛型类、泛型接口、泛型函数
function itself<T>(arg:T):T {
return arg
}
const res = itself<number>(1)
上例子中的T 只是一个类型的变量 传入什么类型那它就是什么,就和函数的形参一样 实参是一个具体的值,而形参只是一个变量名而已,你也可以改成其他的名字,T意为type
1.传入了number后,这个类型就会传递给函数声明时对应的类型变量,你也可以传入其他类型,例:Boolean、String
2.通过泛型就做到了让fn函数与多种不同的类型一起工作,实现了复用 同时保证了类型的安全
简化泛型函数调用
当然我们每次传入<number>还是稍微麻烦,所以在调用时你可以省略<类型>来简化泛型函数的调用
此时,TS内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量Type的类型
比如,下面的例子就会推断出是Boolean
推荐:使用这种简化的方式调用泛型函数,使代码更短,更易于阅读
说明:当编译器无法推断类型或者推断的类型不准确时,就需要显示地传入类型参数
泛型的extends
function getLength(arg) {
return arg.length
}
getLength('奥特曼')
getLength(['迪迦','泰罗'])
getLength(10)
在这段代码中 如果传入了字符串和数组那么是没有问题的 但要传了一个数值类型 我们明确知道是没有length属性的 这时候就要使用泛型对它约束
Type extends xxx 添加泛型约束
function getLength<T extends string | any[]>(arg:T):number{
return arg.length
}
在上面代码中明确要求了T是一个字符串类型或者是任意类型的数组 否则传入其他 编辑器会有红色波浪线
也可以写成{ length: number } 要求传入的类型比如带有 length的属性 并且值是number类型
function getLength<T extends {length:number}>(arg:T):number{
return arg.length
}
当然也可以写成类型或接口的形式
interface constraintLength { length: number }
// type constraintLength ={length: number }
function getLength<T extends constraintLength>(arg:T):number{
return arg.length
}
多个类型变量
泛型的类型变量可以有多个
并且类型变量之间还可以约束(比如,第二个变量受第一个变量的约束 创建一个函数来获取对象中的属性的值)
function getprop<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let person = {name:'jack',age:18}
getprop(person, 'name')
1.添加了第二个类型变量Key,两个类型变量之间使用,逗号分隔
2.keyof关键字接收一个对象类型
3.本实例中 keyof Type 实际上获取的是person对象所有键的联合类型,也就是 'name' | 'age'
4.类型变量Key受Type约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性
function getProperty<Type extends object, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
getProperty(person, 'name')
Type extends object 表示:Type应该是一个对象类型,如果不是 对象类型 就会报错
如果要用到 对象 类型,应该用object,而不是Object
泛型接口
接口也可以配合泛型来使用,以增加其灵活性,增强其复用性
interface IdFunc<Type> {
id: (value: Type) => Type
ids: () => Type[]
}
let obj: IdFunc<number> = {
id(value) { return value },
ids() { return [1, 3, 5] }
}
1.在接口名称的后面添加<类型变量>,那么这个接口就变成了泛型接口
2.接口的类型变量,对接口中所有其他成员可见,也就是接口中所有的成员都可以使用类型变量
3.使用泛型接口时,需要显示指定具体的类型(比如,此处的IDFunc)
4.此时,id方法的参数和返回值都是number;ids 方法的返回值类型是number[]