Typescript的奇妙探险(初体验)

587 阅读8分钟

写在前面

感谢大家百忙之中点击进入这篇文章,因缘和合,我们从网络的万水千山之隔遥聚于此,也相信看到这里的同学有着相似的特点--强烈的求知欲望和持续的学习热情。

可能你们之中会有疑惑,我js用的好好的,为什么前端家庭要融入ts这一成员呢,其一增加学习成本,其二加大人力投入,那ts又有什么好处呢?这里首先与大家分享几个场景

  1. 当我们调用一些封装好的公共组件或函数时,这些组件或方法没有任何注释,为了搞清参数类型,我们是否需要硬着头皮去梳理代码逻辑?
  2. 当我们接手别人的代码并在此基础上进行迭代时,想死的心都有了,是否准备协调测试开发重构代码呢?
  3. 当前端开发时明明已经定义好的接口,等到联调时却发生错误(Cannot read property 'length' of undefined ),我们根据提示信息逐步排查错误,最后发现服务返回参数类型有误,是否会怒气上涌呢?

综上归根结底,javascript是一门动态弱类类型语言,对变量类型的十分宽容,如果我们长期在没有类型判断的思维下开发,很容易养成不良的编程习惯。由此,2014年faceBook推出Flow,而微软推出了typescript,致力于解决静态类型检查,逝者如斯夫,不舍昼夜,随着时间的发展,typescript显然更受市场的欢迎,Angular 和 vue 开始全面使用 ts 重构代码

类型

Js类型

stringnumberboolean
nullundefinedsymbol
objectarrayfunction

Ts新增类型

枚举类型元组类型any类型
void类型Never类型

基本类型

const str:string = " sun "   	       // string
const num:number = 123		       // Number
const flag:boolean = true	       // Boolean
const n:null = null		       // null
const a:undefined|null = unefined      // null || undefined
const sentence: string = `Hello, my name is ${ str }`  // 模版字符串

symbol

const name = Symbol("name");
const  obj = {
    [name] : "sjx"age:18
}

对象类型

Array

const arr1:number[] = [ 1,2,3 ];   		        // 数组元素为number类型
const arr3:(string | number)[] = [ 1,"sun",3 ];		// 元素为 number or string 类型

// 采用泛型的书写形式
const arr2:Array<number> = [1,2,3];

Object

interface NameInfo {
    name: string,        
    age: number
}
const obj: NameInfo = {
    name: "sjx ",
    age : 18,
}

interface后文会有所介绍,心急的小伙伴小伙伴可以直接点击interface一节

复杂数组的书写

interface IPeople {
    name:string,
    age:number,]
    data:IPeople[]
}

const peoples:IPeople[] = [{
   name:'sjx',
   age:18,
   data:[
     {
        name:'qy',
        age:'17'
     }
   ]
}]

Ts新增类型

枚举

其目的是我们可自定义一些带名字的常量,这里主要是为了有一个清晰的概念,后文我们详细介绍枚举这一类型

enum Status {
    success,
    failed,
    pending
}   

元组

可以理解为数组的一种特殊形式,在定义元组时,数组的长度跟元素的类型已经确定,切书写顺序不能紊乱

const tuple:[ string,number,boolean ]  =  [ "sun", 12, true ]

any

typeScript中,任意类型都被可以被归档为 any类型,

const type:any = "string"
const type;any = ['1',2]

上述代码中我们可以看出,使用any类型,无需任何形式的检查,可以很容易地编写类型正确但在运行时有问题的代码。

如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。

void类型

当一个函数没有返回值时,我们可以使用void类型

const consText = (text:string):void => console.log(text)

never类型

表示的是那些永不存在的值的类型, 抛出异常,或根本就不会有返回值的函数表达式

let x: never = msg => { throw new Error(msg) }

枚举

了解枚举之前我们先看如上一段代码,当多条分支需要判断时,我们可以通过封装一个策略对象缓存每个条件对应的方法,根据不同的判断条件执行相应的方法进行优化,此处我们就可以通过常量枚举贮存相应策略

initRole(role:number){
 const func1 = ():void => {console.log()},
     func2 = ():void => {},
     func3 = ():void => {console.log("555")};
 enum Status{
     a = func1(),
     b = func1(),
     c = func2(),
     d = func2(),
     e = func3()
 }
 console.log(Status[role]) 
}

如上,我们代码分工是不是更加明确了?接下来,我们了解一下枚举共有哪些类型

数字枚举

enum Status {
    success,  // 0
    failed,   // 1
    pending   // 2
}   

数字的枚举的原理为反向映射,即 status[ status['success']=1 ] = 'success'

当我们打印Status[success] ,输出0,当我们打印Status[0],输出success

数字枚举还可以自定义枚举值

enum Status {
    success = 1,		// 1
    failed,		        // 2
    pending			// 3
}   

字符串枚举

enum Status {
    UPLOADING = "request_loading",
    SUCCESS = "request_success",
}

当我们使用公共状态管理工具(Vuex,Redux、Mobx)时,需要将状态标识抽离,此时可以利用字符串枚举贮存所有的状态

异构枚举

字符串枚举和数字枚举混合使用,就是异构枚举,但不推荐使用

enum Status {
   UPLOADING = 0,
   SUCCESS = "request_success",
}
// 对应 es5 的代码如下
var Status;
(function (Status) {
   Status[Status["UPLOADING"] = 0] = "UPLOADING";
   Status["SUCCESS"] = "request_success";
})(Status || (Status = {}));

通过对比可以发现,字符串枚举是没有反向映射的

常量枚举

常量枚举使用后不会编译出一个枚举对象,只是做一个贮存状态的作用

const enum Status {
    UPLOADING = 200,
    SUCCESS = 100,
    FAILED = 401,
}
console.log(Status.SUCCESS )

当我们封装axios、fetch时,可以用常量枚举来贮存状态码

接口

接口提供了一种用以说明一个对象应该具有哪些方法的手段, typescript中接口是一个非常灵活的概念,常用于对【对象的元素类型】进行描述

对象

定义一组对象

interface INameInfo {
    name : string,        
    age : number,  
}
const nameInfo:INameInfo = {
    name:'sjx',
    hobby:'hhh'
}

可选|只读属性

interface Person {
    readonly name: string;   // readonly 该参数只读
    age?: number;	    //  ?  该参数非必传参数
}

数组

interface INameInfo {
    name : string,        
    age : number,  
}
const list:NameInfo[] = [{
    name:'sjx',
    hobby:'hhh'
},
{
    name:'sjx',
    age:18,
    hobby:'hhh'
}]

字符串索引签名

索引接口时,当我们定义的对象传入多余的属性时,会提示tyepscript的错误,此时需要用到字符串索引签名

interface NameInfo{
    name: string,
    [id:string]: any
}

const obj:NameInfo = {
    name:'sjx',
    age:18,
    hobby:'hhh'
}

任意的字符串去索引 NameInfo ,可以得到任意的结果 any

函数

函数参数校验

interface NameInfo{
    name: string,
    age?: number
}

// 参数必须是一个对象,且必须有 name 属性
const showInfo = (data:NameInfo) => console.log(data.name)  

定义函数体

函数体使用type定义

type showType = (data:NameInfo) => void    // 参数索引符合 NameInfo 接口,且返回值类型为 void

const showInfo:showType = (data) => { console.log(data.name) }

函数重载

typescript允许使用function定义多个函数名相同的函数,根据传入参数的不同,调用不同的处理方法,返回不同的结果

function showInfo(...args: string[]): string
function showInfo(...args: number[]): number

function showInfo(...args:[]):any {
   const first = args[0]

   if ( typeof first === "string") {
        return args.join("")
   } else {
        return args.reduce((pre,cur) => cur+pre, 0)
   }
}

接口继承

接口继承使用extends字段

interface PeopleType{
    name: string,
    age: number
}

interface Sun extends PeopleType{  // Sun 继承 PeopleType
    hobby: string
} 		

const fund:Sun = {
    name:'sjx',
    age:18,
    hobby:'chi'
}

绕过多余属性检查

什么是多余属性检查?

如图,IName定义了name,age,当我们根据IName接口索引新建对象peopleOne时,发现该peopleOne定义了hobby字段,此时产生错误提示。解决方式主要为类型断言、索引类型、类型兼容性

索引类型

interface NameInfo{
    name: string,
    age?: number,
    [prop: string]: any
}

类型断言

手动指定一个值的类型

showInfo({
    name:'sjx',
    hobby:'eat'
} as IName)

类型兼容性

const obj = {
   name:'sjx',
   hobby:'chi'
}
showInfo(obj)

高阶类型

联合类型

一个值可以是几种类型之一

let tsUnion: number | string = 1;

交叉类型

将多个类型合为一个类型

interface TypeA {
   name: string 
}
interface TypeB { 
    age: number 
}
let c: TypeA & TypeB = { 
   name:'sjx',
   age:18
};

类型断言

手动指定一个值的类型 值 as 类型(建议)

type func = (target: string|number ) => number

const getLength:func = target=> {
    if ((target as string)) return (target as string).length 
    return target.toString().length
}

变量后使用 !

变量后使用 !:表示类型推断排除null、undefined

/**
* @description: 给输入的数值添加前缀
*/
const getSp = ( num: number|null ): string => {
      const getSnap = ( prefix: string ) => prefix + num!.toFixed().toString()
      num = num || 0.1   
      return getSnap("lison-")
}

类型保护

使用类型断言

if ((arg as IHobby).sport) {    
    return (arg as IHobby).sport  
}

typeOf

const getLength = (arg: number | string):any => {
    return  typeof arg === "string" && arg.length
}

instanceOf

const getLength = (arg: number | any[]):any => {
     return  (arg instanceof Array) && arg.length
}  
// “instanceof”表达式的左侧必须是“any”类型、对象类型或类型参数。

自定义

const isCat = (params:IDog|ICat):params is ICat => params.hasOwnProperty("color")

const animal = (data:IDog|ICat):any => {
    if(isCat(data)) return data.color
}

(字符串|数字)字面量

字符串

type Name = "sjx"|"zqm"|"hwh"|"lj"

const name: Name = "sjx" 

const getName = (name: Name): void => console.log(name)

数字

type Age = 18 | 22

interface IAge < T > {
   name: T,
   age: Age,  // 根据接口定义age时,只能取值 18 或 22
}
 
const person:IAge <string>= {
    name:'sjx',
    age:18
}

React函数式组件

import React from 'react'

interface Props {
 		name ?: string
}

const InitPage:React.FC<Props> = props => {
   return (
       <div>
           初始化首页{props.name}
       </div>
   )
}

InitPage.defaultProps={
 		name:'sjx'
}

export default InitPage

React类组件

import React, { Component } from 'react'

interface Props{
     age?: number|undefined
}

interface State{
     name: string|undefined,
}

export default class index extends Component<Props,State> {
     state:State = {
         name:"sjx"
     }
     static defaultProps = {
         age: 18
     }

     render() {
         const {name} = this.state,{age} = this.props
         return (
             <div>
                 姓名:{name}
                 年龄:{age}
                 年龄18一朵花
             </div>
         )
     }
}

高阶组件

import React, { Component } from 'react'

function ShowCompoment(com){
 return class extends Component{
     render(){
         const {loading} = this.props
         return loading ? <div>loading....</div> :<com {...props}/>
     }
 }
}

使用Ts后

import React, { Component } from 'react'

interface ShowLoading {
   loading : boolean
}

function ShowCompoment<P>(Com:React.ComponentType<P>){
 return class extends Component<P & ShowLoading>{
     render(){
         const {loading} = this.props
         return loading ? <div>loading....</div> :<Com {...this.props as P}/>
     }
 }
}

export default ShowCompoment
  • Com:React.ComponentType: 指定被包装组件的类型,属于React预定义的类型

  • P: 被包装组件的属性类型

  • ShowCompoment:传入被包装组件的属性类型

  • 被包装组件没有loading属性,需要定义loading属性。

    interface ShowLoading {
        loading : boolean
    }
    

    使用交叉泛型,让被包裹的组件有loading属性

    Component<P & ShowLoading>s
    

请求

请求方法

import ajax from "@utils/request"

interface ILogin {
 name:string,
 pwd:number,
 [prop: string]: any
}

const login = (data:ILogin) => ajax.post("/test/login",data) 

组件请求

import { getPermission } from '@server/user'
import asyncRequest from '@utils/resCatch'

interface IReqLogin {
    order: 9,
    path:string,
    type:string,
    code:string,
    childs:IReqLogin[]
}

useEffect(() => {
   const resData = {
       name:'sjx',
       pwd:123446
   }
   asyncRequest(async()=>{
       const res:IReqLogin = await getPermission(resData)
       setMenu(res)
   })
}, [])