全文为本人学习的随笔,以官网提供的内容为准,参考不可保证其正确性与权威性。
1.ts与js的关系
(1) TypeScript如何运行 TypeScript -编译-> JavaScript -运行-> 浏览器。 编译过程出错的代码,不会转化为JS。 但其下一行代码依然正常编译,转化为JS。 也就是说TypeScript本身是不运行的,是编译为JavaScript运行。 TypeScript一般借助编译器或脚手架的开发环境进行编译运行。 例如:webpack+belel、Vite 2、typescript compiler。 因为开发环境不够轻便,所以建议使用codesandbox提供的线上开发环境进行入门学习。
(2) TypeScript与JavaScript的区别 TS = JS: type TS在JS的基础上增加了类型。 类型会在完成编译后删除,并转化为浏览器上可运行的JS
(3)ts与tsx的关系 tsx = ts + jsx 能够使用ts与jsx的语法。
2.类型声明
(1) ts变量声明方式
const name = 'Ogas'
//js声明方式
const name: string = 'Ogas'
//ts声明方式
ts声明变量时,需要在变量名后用:连接该变量的数据类型。 即ts声明变量的同时,指定了变量的基本类型。
(2) 类型的作用
if(theNumebr * otherNumbe > 0){
return 1
}else{
return -1
}
上述是将两个非零number变量相乘,得到正数返回1,得到负数返回0。 我们需要经过计算结合判断才能知道,其结果是正数还是负数。 但如果提前指定两个number的类型,则可以省略计算这一步。
let theNumber: 正数
let otherNumber: 负数
上述是一段伪代码。 如果提前指定两个number的类型,细分为正负。 那么不通过计算我们可以指定number的类型,从而通过类型判断结果。 这样能够减少一步计算量。 这就是类型声明的作用。
(3) 类型运算的代码纠错
const a = '1'
const b = 1
console.log(a+b)
上述代码实际上是不应当有计算结果的。 但是js松散的语法依然能够通过,得到计算结果为11。
const a: string = '1'
const b: number = 1
如果a、b变量是声明类型的ts变量。 string与number无法进行运算。 那么在a+b时则会因为无法通过ts的类型检测而报错。
(4) 属性引用的代码纠错
import ReactDOM from 'react-dom'
ReactDOM.rener()
在将render()误写成rener时, ts会通过typeof去判断rener是否在ReactDOM上存在从而纠错。
3.复杂类型声明
(1) 对象类型基本声明
const a: undefined = undefined
const b: null = null
const c: string = 'hi'
const d: boolean = true
const e: symbol = Symbol('hi')
const f: bigint = 123n
我们目前可以通过上述方式声明7中简单数据类型变量。
const obj: Object = {}
const arr: Object = []
但是如果用类似方式声明对象则会有问题。 所有的对象都是Object构造产生并引用相同的原型, 指明对象的数据类型没有太多意义。
const Ogas: Object = {
name: 'Ogas',
age: 18,
sex: male,
}
const Mercury: Object = {
name: 'Mercury',
age: 18,
sex: female
}
上述是两个对象,他们拥有相同的属性构成因此是属于同一个类。 将他们属于的这个类称为Person。 此时对于对象来说,Object是它们的大类,而Person是Object细分下的小类。 因此使用Person类作为对象的类型,更能够准确的表示。
const arr: Object = {}
const arr: Array = []
因此引出数组声明类型时,因当写为Array,而Array是一个类。
(2) 数组类型基本声明
const arr1: Array = [1,2,3]
const arr2: Array = ['Ogas','安哥','流星胖胖','东哥']
通过声明类型为类Array能够将数组从Object细分出来, 但是对于arr1,arr2的区别并没有做到更加细分, 显然arr1是一个number数组,arr2是一个字符串数组。
const arr1: Array<number> = [1,2,3]
const arr2: Array<string> = ['Ogas','安哥','流星胖胖','东哥']
在Array后通过<>包裹写入成员的数据类型, 这样能够在数组进行类型声明时,更加准确细分。 js数组成员数据类型并不单一, 如果成员数据类型有多种则需要向<>写入条件。
const arr1: Array<number | string> = [404,'未知错误',401]
通过<>可以对数组成员的数据类型进行约束。
(3) 函数类型基本声明 与数组同理,声明函数的类型为Function类没有意义。 函数的参数的类型与返回值的类型才能决定函数的类型。
// const add = (a, b) => a+b
const add = (a: number, b: number): number => a+b
可以直接在参数后接上数据类型,在参数的()后接返回值的数据类型。
const add: (a:number, b: number) => number = (a,b) => a+b
第二种写法是在函数名后将参数类型与返回值类型写成箭头函数, 在箭头函数后用=连接函数体。 上述两种写法都存在问题,类型声明与函数体在编写上太紧密, 影响了函数的可读性,因此有第三种将类型与函数体分离的写法。
type Add = (a: number, b:number) => number
const add: Add = (a, b) => a+b
将函数的类型单独声明为一个类型, 类型名首字母大写, 在声明add函数时则直接使用Add类型。 这样实现了函数类型与函数体分离。
(4) 动作函数类型声明 但需要声明一个函数的类型是无参数无返回值的动作函数时,可以写成
const onClick: () => void = function(){
console.log('我是无参数无返回值的函数。')
}
当返回值的类型声明为void时,规范上函数体中不能有return。
(5)含有属性的函数类型声明 js中函数是函数的同时也是对象。
const add: Add = (a, b) => a+b
add.value = 0
type Add = (a: number, b:number) => number
上述是add函数,同时add函数内有一个value属性,值为0。 当add函数是一个有属性的函数时, 类型Add就很难去精准的表述目标函数add。 这时需要借助interface。
interface AddWithProps{
(a: number, b: number): number
value: number
}
interface关键字后接类型名, 用{}包裹类型具体内容, (a: number, b:number) => number 在interface中, 需要将=>改写为:, value: number则声明了其含有属性value,类型为number。
4.TypeScript额外数据类型
(1) any
let a: any = 'hi'
a = 1
any是任意数据类型,即不对变量进行任何数据类型的约束。
(2) unknown
let b: unknown = JSON.parse(theData)
unknown一般用于提前无法得知的数据类型, 例如通过接口获得了一个theDate字符串, 将其通过JSON.parse转换后,无法确定其数据类型。
(3)any与unknown的区别
let a: any = 'hi'
console.log(a.value)
a变量的类型声明为any时, 即便a.value不存在,ts也不会报错。
let b: unknown = JSON.parse(theData)
b.value
b变量的类型声明为unknown时, b.value则会报错,因为未知的类型不确定是否有属性value。
(4)as 断言关键字
let b: unknown = JSON.parse(theData)
type TheData = {
value: string
}
console.log((b as TheData).value)
as的行为被称作断言。 as可以将类型为unknown的变量当做某种类型来处理。 例如上述声明了类型TheData, 当要输出b.value时会因为unknown类型而报错, 通过as关键字,可以让输出b.value时, 将b当做TheData类型处理,在执行这句语句时, ts会认为b是TheData类型。
(5) never 类型never表示不应该存在的类型。 类型为never的变量不可用,只有在数据类型错误的时候出现。
(6)元组 元组是不可变更长度的数组。
type Point = Array<number>
const point = [23,312]
point变量表示一个x y坐标系上的坐标, 显然二维坐标是长度为二的数组,类型Point无法准确表示长度。
type Point = [number,number]
上述将Point声明为一个元组,长度仅限为2,成员1为number,成员2为number。 元组只是ts中存在的语法概念,在js中不存在。 元组本质依然是js数组,也就是如果绕过ts语法,依然能对数组进行push。
(7) enum 枚举 枚举是一种相对特殊的类型。
enum Dir {top,bottom,left,right}
如上声明了一个类型名称为Dir的枚举类型。
let d1: Dir = Dir.top
let d2: Dir = Dir.left
声明了d1,d2两个为Dir枚举类型的变量。 枚举变量在赋值时只能赋值为其枚举,并要写明前缀, 例如d1,d2只能赋值为top,bottom,left,right,且需要加前缀Dir., 但变量d1,d2的值不是top,bottom,left,right。 当d1赋值为Dir.top时,值为0,赋值为Dir.bottom时,值为1,以此类推。 枚举类型使用场景很少,且使用不方便。 枚举类型主要是来自其他语言的概念,方便多语种开发者使用。
(8) void 类型空 类型空与空类型不同,void仅在类型上表示为空。 而null在意义上表示未空,实际存在类型,类型为null。
*4.TypeScript扩充后的数据类型列举
-
null
-
undefined
-
number
-
string
-
boolean
-
symbol
-
bigint
-
object ( class, type, interface )
-
any
-
unknown
-
void
-
never
-
Enum
-
元组
5.联合类型
类型间并得到联合类型。
const fn1 = (n: string | number) => {}
针对fn1的形参n,其类型可以是string,也可以是number。 string | number就是一个string与number并得到的联合类型。 即fn1的形参n的类型既可以是string,也可以是number。
type A = {
name: string
age: number
}
type B = {
name: string
id: number
}
上述类的类型也能进行并得到联合类型。
const fn1 = (n: string | number) => {
n.toFixed()
}
上述情况,n.toFixed()会报错。 .toFixed()是number上存在的api,不存在与string上。 因此.toFixed()对于联合类型string | number来说不存在。 解决这种情况的方式是根据n的类型进行重载。
const fn1 = (n: string | number) => {
if(typeof n ==== 'number'){
n.toFixed()
}
}
这样确保了n的类型是number时,才会执行n.toFxied()。
type A = {
name: string,
age: number
}
type B = {
name: string,
id: number
}
const fn1 = (n: string | number) => {
console.log(n.age)
}
n.age会报错,原因与上一个案例一样。 此时A与B无法通过typeof关键字去区分而无法重载, 这时需要对A、B作区分。
type A = {
type: 'A',
name: string,
age: numebr
}
type B = {
type: 'B',
name: string,
id: id
}
const fn1 = (n: string | number) => {
if(n.type === 'A'){
console.log(n.age)
}else {
console.log(n.id)
}
}
将A、B两种类型做差异的方式有很多种, 写入相同的属性作固定的赋值为方式之一, 而type被称作联合类型的key, 如何做差异需要根据具体的使用情况去分析。
5. 交叉类型
类型间交得到交叉类型。
type A = number & string
A是number与string交得到的交叉类型A。 而A的类型为never,因为不存在即是number又是string的类型。 因此通过简单类型得到的交叉类型没有意义。 交叉类型往往是针对对象,通过类的交叉得到。
type A = {name: string} & {age: number}
/*
type A = {
name: string,
age: number
}
*/
上述是对类的交得到的交叉类型。
type A = {
name: string,
id: number
} & {
name: string,
id: string
}
上述得到的交叉类型为never, 因为id: number 与 id: string 的交是冲突的。
6. 声明标签节点类型
const div1: HTMLDivElement
标签节点本质是对象下的类,因此可以作为类型。 而这些标签节点的类来自于DOM。
7.泛型的概念
type A = 'Ogas' | 404
//ts
var a = ['Ogas',404]
//js
a与A虽然表示的东西含义不一样, 但是两者在代码编写上相似, 从编写上看,联合类型就像js的数组, 而泛型在ts中就像js的函数。
type F<T> = T | [T]
类型F需要接受一个类型T, 根据接受的类型T得到T, 或者T组成的数组。 从编写上泛型像是js的函数, 接受类型返回新的类型。 从意义上看,泛型像vue的计算属性。 泛型不能直接作为类型使用,需要接受类型参数得到新类型, 并将得到的新类型声明后才能使用。 即泛型不传入类型无法使用。
*7.1泛型的意义
const add(a,b) => {
return a+b
}
上述是个简单的add函数,我们希望当a,b都是number时,返回运算结果。 而a,b都是string时,返回两者的字符串拼接。 这时对于add的类型就相对复杂了。
type Add<T> = (a: T, b: T) => T
如果使用泛型就能够得出。
实现了add函数的类型需求。 这时会产生问题,add()需要参数必须是number或string类型。
type Add<T> = (a: T, b: T) => T
type AddNumber = Add<number>
type AddString = Add<string>
将number,string作为Add的参数, 分别得到add()在参数类型不同的两个重载的类型。 这里泛型对a,b的类型一致性作出约束,都必须是T。 同时泛型让返回值与参数一致,也都是T。
const addN: AddNumebr = (a,b) =>{
return a+b
}
const addS: AddString = (a,b) =>{
return a+b
}
在编写中不会在声明泛型时传入类型, 而是在使用泛型时传值。
const addN: Add<number> = (a, b) => a+b
const addS: Add<string> = (a, b) => a+b
在实际应用中,泛型与函数在形式上一样。 函数往往会多次调用并将返回值作为另一个函数的参数, 而泛型也会被多次使用,将其得到的类型作为另一个泛型的参数。
*7.2泛型的问题
type Add<T> = (a: T, b: T) => T
type AddNumber = Add<number>
type AddString = Add<string>
上述通过该方式实现了对add()的重载, 但他将函数的重载单独拿出成为了一个函数。
const add(a,b) => a+b
const addN: Add<number> = (a, b) => a+b
const addS: Add<string> = (a, b) => a+b
因为泛型将参数类型缩小,反而将原有的函数拆分成了两个函数。
type Add<T> = (a: T, b: T) => T
const add: Add<number | string> = (a, b) => {
return a+b
}
上述代码看上去很合理,实际上并没有对a,b的类型一致性进行约束。 从范式的编写上看,a,b的类型与返回值都是T,似乎进行了一致性的约束, 但实际上T不是number或string其一,而是number | string这个联合类型。
*7.3函数重载类型
function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a: any, b: any): any {
return a + b
}
functions关键字用于声明函数的重载类型, 如果将function关键字更改为type,那么则会类型重名, 但因为使用了function关键字,针对的是函数的重载,因此不会重名。 前两行是声明函数的两种重载类型, 而第三行开始是声明函数体,其遵循前两行的类型约束。 通过这种方式就解决了上述提到的问题。
function get(options: {header: string}, url: string,): void
function get(options: {url: stirng} & {headers: any}): void
function get(options:any, url?: string){
if(arguments.length === 1){
//参数仅有options的情况
}else{
//参数为url和options的情况
}
}
axios.get({
header: 'get'
},'url')
axios.get({
url: '/xxx',
header: 'get'
})
在前后端交互中会经常出现如上情况, 这时可以用function关键字声明get的函数重载类型。 而get的传参数量不是固定的, 可以将url作为一个参数,也可以将url写入在options参数中, 这样在写完函数重载类型后,写函数体时, 需要在不必须的参数url后写上?,表示该参数不一定存在。