这是我参与「第五届青训营 」伴学笔记创作活动的第4天
一、本堂课主要内容
- TypeScript历史
- TypeScript优势
- TypeScript语法与使用
- TypeScript工程应用
二、主要内容
发展历史
- 2012-10,微软发布TypeScript第一个版本(0.8)
- 2014-10,Angular发布了基于TypeScript的2.0版本
- 2015-04,微软发布了Visual Studio Code
- 2016-05,@types/react发布,TypeScript可开发React
- 2020-09,Vue发布了3.0版本,官方支持TypeScript
- 2021-11,v4.5版本发布
TypeScript优势
js和ts的最大区别就是js是动态类型,ts是静态类型。就是说js只有在运行时才能确定一个变量的类型,而ts在开发时就能确定,就像java那样。优势如下:
- 可读性增强,能清楚的知道一个变量的类型,且开发时就有代码提示,开发更顺畅。
- 可维护性增强,因为变量类型已确定,大大减少赋错值的情况,可以在编译阶段就暴露大部分错误
- 为js的超集,包含兼容所有js特性,支持共存,支持渐进式引入与升级
基本语法
练习的话,可以去官网在线编辑器练习,也可以本地vscode编辑器里创建一个ts文件,直接使用,当然要是想把ts编译为js的话还得另外安装包,这一点可以见工程应用
基础数据类型
const a: number = 0
const b: string = 'a'
const c: boolean = false
const d: null = null
const e: undefined = undefined
就是在变量后面加 冒号+类型,而且如果定义变量的同时还带初始化,不需要显示的定义变量类型的,ts会自动判断
对象类型
interface IPerson {
// 只读属性,就是说变量定义之后就不能修改了,要赋值的话只能在定义变量的时候初始化
readonly id: string
name: string
age: number
// 值必须在这3个里面选
gender: 'male' | 'female' | 'secret'
// 可选类型,该属性可不存在,上面几个没带?的都是必须存在的
role?: string
// 任意属性,key的取名不重要,重要的是key的类型和key后面值的类型
// 注意这个值类型得包括上面几个属性的值的类型,不是指除上面几个属性外其他属性的值的类型
[key: string]: any
}
// 继承,Student包含Person定义的所有内容
interface IStudent extends IPerson {
// 可改写属性,但不能冲突,比如 role: number 就会冲突
role: 'student'
// 正常添加属性
height: number
}
可以发现,似乎每个类型前面都加了个I,这个是规范(不强制),主要是方便区分类型和变量。
还有一个type,类型别名,简单来讲就是换个名字
type IAnyType = any
type IDomTag = 'html' | 'body' | 'div'
type IPerson = {
// 只读属性,就是说变量定义之后就不能修改了,要赋值的话只能在定义变量的时候初始化
readonly id: string
name: string
age: number
// 可选类型,该属性可不存在,上面3个没带?的都是必须存在的
gender?: 'male' | 'female' | 'secret'
role?: string
// 任意属性,key的取名不重要,重要的是key的类型和key后面值的类型
// 注意这个值类型得包括上面几个属性的值的类型,不是指除上面几个属性外其他属性的值的类型
[key: string]: any
}
函数类型
interface IAdd {
(x: number, y: number): number
}
const add1: IAdd = (a, b) => a + b
function add2(a: number, b: number): number {
return a + b
}
函数重载,简单来讲就是多个调用方法,多个代码提示,下面这个就有2个重载
interface CusTomMessage {
(content: string | number): void;
(content: string | number, duration: number): void;
(content: string | number, key: string): void;
(content: string | number, duration: number, key: string): void;
}
const customMessage: CusTomMessage = (content: string | number, duration?: any, key?: any) => {
if (typeof duration === "string" && (typeof key === 'undefined')) {
key = duration
duration = null
}
message.open({
type: 'success',
className: 'msgSuccess',
content: content,
duration: duration || 2,
key: key
})
}
注意兼容哦,customMessage这个函数的参数属性得兼容4个重载的任何一个。可以试试改改实现时duration的类型
数组类型和枚举类型
// 数字数组
type IArr1 = number[]
// Record相当于IArr4那样的简写,IArr4写成Record就是 Record<number, any>,键的类型和值的类型
// 类型为数组,里面的元素只能是number和这个Record
type IArr2 = Array<number | Record<string, number>>
// 元组,数组必须4个元素且对应位置类型相同
type IArr3 = [string, number, boolean, string]
// 接口表示
interface IArr4 {
[key: number]: any
}
const arr2 = [1, 2, {a: 3}]
const arr4 = [0, '1', () => '2', false, null]
// 枚举类型,主要用在要多个值,但对于值没什么要求(不重复就行)的场景
// 简单来讲就是不需要记忆该填什么什么样的值,直接一个类型一个点,然后慢慢挑
enum EnumColor {
// 初始值默认为0,后续不赋值时会递增
blue, // 0
cyan, // 1
purple = 4, // 4
orange, // 5
red = 'red', // 'red
green = 'green' // 'green
}
EnumColor['blue'] === EnumColor.blue
EnumColor.blue === 0
泛型
定义时不指明类型,使用时再指定类型。
下面例子,这个函数就是返回数组,数组每个元素都是target,因为target类型不定,可以为number、string或者其他的,对应的返回值类型也就是number[]、string[]等,都不能确定,按照上面的方法就只有IGetRepeatArr这样的接口了,但明显这样不行,体现不出输入和输出之间的关系。再看后面一种写法,暂定一个不确定的类型T,然后target类型为T,返回值类型为T[],这里T会根据使用时target的类型来确定,这样就能确定输入输出之间的关系了。
// 输入可以为任何类型
function getRepeatArr(target) {
return new Array(100).fill(target)
}
type IGetRepeatArr = (target: any) => any
type IGetRepeatArrR = <T>(target: T) => T[]
再来个类似的
type IGetRepeatArrR2<T> = (target: T) => T[]
与上面的IGetRepeatArrR相比,发现<T>挪到前面了,这时使用时就不是自动根据target来确定类型了,而是使用时手动传入类型
type IGetRepeatArrR = <T>(target: T) => T[]
type IGetRepeatArrR2<T> = (target: T) => T[]
const getRepeatArr: IGetRepeatArrR = (target) => new Array(100).fill(target)
const getRepeatArr2: IGetRepeatArrR2<number> = (target) => new Array(100).fill(target)
其他的使用方式:
// 限制T必须为string类型的
type IGetRepeatArrR3 = <T extends string>(target: T) => T[]
// 不手动传入类型是,默认就用number
type IGetRepeatArrR4<T = number> = (target: T) => T[]
类型断言
// 对象数组,每个对象中要有key属性且类型要为string
type IObjArr = Array<{
key: string
[objKey: string]: any
}>
// 将数组转换为类数组的形式,如 [{a: 1}, {a: 2, b: 1}] => { '0': { a: 1 }, '1': { a: 2, b: 1 } }
function keyBy<T extends IObjArr>(objArr: Array<T>) {
const result = objArr.reduce((res, val, key) => {
res[key] = val
return res
}, {})
// 因为reduce传入的初始值为{},所以result类型就是{},但实际上我们可以很明确的知道result的类型为Record<string, T>
// as:类型断言,明确指定result的类型不是别的就是Record<string, T>
return result as Record<string, T>
}
高级类型
联合/交叉类型
看这个例子,书的数组,通过type来区分书的类别
const bookList = [{
author: 'xiaoming',
type: 'history',
range: '2001-2021'
}, {
author: 'xiaoli',
type: 'story',
theme: 'love'
}]
写出一个符合它的类型
// 有的人可能这么写
interface IHistoryBook {
author: string
type: 'history'
range: string
}
interface IStoryBook {
author: string
type: 'story'
theme: string
}
type IBookList = Array<IHistoryBook | IStoryBook>
// 也有的人可能这么写
type IBookList = Array<{
author: string
type: 'history' | 'story'
theme?: string
range?: string
}>
可以看到,第一种写法缺点就是太繁琐了,为了一个变量类型写了这么多的类型定义,而且类型声明中还存在着较多重复。第二种写法就是不够具体,比如history的书没有theme属性,story的书没有range属性
type IBookList = Array<{
author: string
} & ({
type: 'history',
range: string
} | {
type: 'story',
theme: string
})>
这里的|和&就是或、与,分别表示类型为多者之一、类型叠加
类型守卫
interface IA { a: 1, a1: 2 }
interface IB { b: 1, b1: 2 }
function log2(arg: IA | IB) {
if(arg.a) {
console.log(arg.a1)
} else {
console.log(arg.b1)
}
}
初衷是arg为IA的话就输出arg.a1,否则输出arg.b1,但实际上这段代码放到编辑器里面会报错的,原因是arg为 IA | IB 类型的,直接取属性a不一定取得到
function getIsIA(arg: IA | IB): arg is IA {
return !!(arg as IA).a
}
function log2(arg: IA | IB) {
if(getIsIA(arg)) {
console.log(arg.a1)
} else {
console.log(arg.b1)
}
}
这里定义一个类型守卫,指出arg就是IA类型的,当然这样子显得很多余,毕竟要多写一个无用的函数。有的情况编辑器会自动推断类型的,比如上面的书的案例,book.type === 'history'就能明确确定是history书,不必多写一个函数
其他的类型
// keyof,取key值
type IKeys = keyof {a: string, b: number} // "a" | "b"
// Partial,T类型的一部分,即类型和T一样,但是所有属性都是可选的
// in和js中的in的用法类似
type IPartial<T extends Record<string, any>> = {
[P in keyof T]?: T[P]
}
// 取函数的返回值类型
// 这里extends表示类型推断,表达类似于 T === 判断类型 ? 类型A : 类型B
// infer表示定义类型变量,在这里返回值的类型用泛型R表示
type IReturnType<T> = T extends (...args: any[]) => infer R ? R : never
// 同样,这个表示取数组中单独一个元素的类型
type IArrayItemType<T> = T extends (infer S)[] ? S : never
// asserts,断言,这里e为一个html元素,但是我知道它就是一个输入框
function assertIsInput(e: any): asserts e is HTMLInputElement { }
工程应用
浏览器端:
NodeJS:
三、个人总结
知识点还是不少的,基础语法得掌握,高级类型可以不用死磕,用到的过程中慢慢理解就行。其实大多数场景下用的还是基础语法。
要想熟练的使用typescript,可以直接就搬上来用,写js代码时可以直接更进一步写ts,在练习中成长,实在是不知道该怎么定义一个类型的话,any大法,不知道怎么取属性的话,也可以先把这个变量断言为any,再取属性。