[TypeScript] 一文式TypeScript入门级教程

187 阅读12分钟

全文为本人学习的随笔,以官网提供的内容为准,参考不可保证其正确性与权威性。

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后写上?,表示该参数不一定存在。