最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Typescript开发学习总结(附大量代码)

    正文概述 掘金(飞灰)   2021-03-08   375

    如果评定前端在最近五年的重大突破,Typescript肯定能名列其中,重大到各大技术论坛、大厂面试都认为Typescript应当是前端的一项必会技能。作为一名消息闭塞到被同事调侃成“新石器时代码农”的我,也终于在2019年底上车了Typescript。使用的一年间整理了许多的笔记和代码片段,花了一段时间整理成了下文。

    本文不是教程,主要目的是分享我个人在使用Typescript开发1年期间的一些理解和代码片段,因此文章内容主要围绕对某些特性做的研究和理解。也希望能帮到一些同在学习使用Typescript的小伙伴,如有错误遗漏也希望能够指出。

    基础数据类型

    Javascript一共有6种基础类型:String/Number/Boolean/Null/Undefined/Symbol,分别对应Typescript中6种类型声明:string/number/boolean/null/undefined/symbol

    基础数据类型的类型声明适用的几条规则:

    1. Typescript在编译时会对代码做静态类型检查,多数情况下不支持隐式转换,即let yep: boolean = 1会报错
    2. Typescript中的基础类型声明的首字母不区分大小写,即let num: number = 1等同于let num: Number = 1,但是推荐小写形式
    3. Typescript允许变量有多种类型(即联合类型),通过|连接即可,如let yep: number | boolean = 1,但是不建议这么做
    4. 类型声明不占用变量,因此let boolean: boolean = true是允许的,但是不建议这么用
    5. 默认情况下,除了neverTypescript可以把其他类型声明(包括引用数据类型)的变量赋值为null/undefined/void 0而不报错。但这肯定是错误的,建议在tsconfig.json中设置"strictNullChecks": true屏蔽掉这种情况
    6. 对于基础类型而言,unknownany的最终结果是一致的
    // 字符串类型声明,单引号/双引号不影响类型推断
    let str: string = 'Hello World';
    
    // 数字类型声明
    let num: number = 120;
    // 这些值也是合法的数字类型
    let nan: number = NaN;
    let max: number = Infinity;
    let min: number = -Infinity;
    
    // 布尔类型声明
    let not: boolean = false;
    // Typescript只对结果进行检查,!0最后得到true,因此不会报错
    let yep: boolean = !0;
    
    // symbol类型声明
    let key: symbol = Symbol('key');
    
    // never类型不能进行赋值
    // 执行console.log(never === undefined),执行结果为true
    let never: never;
    // 但即使never === undefined,赋值逻辑仍然会报错
    never = undefined;
    
    // 除了never,未开启strictNullChecks时,其他类型变量赋值为null/undefined/void 0不报错
    let always: boolean = true;
    let isNull: null =  null;
    // 不会报错
    always = null;
    isNull = undefined;
    

    引用数据类型

    Javascript的引用数据类型有很多,比如Array/Object/Function/Date/Regexp等,与基础类型不一样的地方是,Typescript有些地方并不能简单地与Javascript直接对应,部分的执行结果让人摸不着头脑。

    在书写规则上,除了Object以外,Typescript其他的引用数据类型声明的首字母必须大写,如let list: array<number> = [1]会报错,必须写成let list: Array<number> = [1]。原因是这些引用数据类型在本质上都是构造函数,Typescript的底层会通过类似于list instanceof Array的逻辑进行类型比对。

    其中比较有意思的一个点是:在所有的数据类型里,Array是唯一的泛型类型,也是唯一有两种不同的写法:Array<T>T[]

    与数组相关的类型声明还有元组Tuple,跟数组的差别主要体现在:元组的长度是固定已知的。因此使用场景也非常明确,适合用在有固定的标准/参数/配置的地方,比如经纬度坐标、屏幕分辨率等。

    // 数组类型有Array<T>和T[]两种写法
    let arr1: Array<number> = [1]
    let arr2: number[] = [2]
    
    // 未开启strictNullChecks时,赋值为null/undefined/void 0不报错
    let arr3: number[] = null
    // 编译时不会报错,运行时报错
    arr3.push(1)
    
    // 元组类型
    // 坐标表示
    let coordiate: [ number, number ] = [114.256429,22.724147]
    
    // 其他引用数据类型
    let date: Date = new Date()
    let pattern: Regexp = /\w/gi
    
    // 类型声明在函数中的简单运用
    // 函数表达式的写法
    function fullName(firstName: string, lastName: string): string {
      return firstName + ' ' + lastName
    }
    // 函数声明式的写法
    const sayHello = (fullName: string): void => alert(`Hello, ${ fullName }`)
    
    // 当你不知道函数的返回值,但又不想用any/unknown的时候可以试试这种类型声明的写法,不过不推荐
    const sayHey: Function = (fullName: string) => alert(`Hey, ${ fullName }`)
    

    Typescript中关于对象的类型声明一共有三种形式:Object/object/{},我一开始以为Object会像Array也是泛型类型,然而经过测试发现不仅不是泛型,还有个首字母小写形式的objectObject/object/{}三者之间的执行结果完全不同。

    1. Object作为类型声明时,变量值可以是任意值,如字符串/数字/数组/函数等,但是如果变量值不是对象,则无法使用其变量值特有的方法,如let list: Object = []不会报错,但执行list.push(1)会报错。造成这种情况的原因是因为在Javascript中,在当前对象的原型链上找不到属性/方法时,会向上一层对象进行查找,而Object.prototype是所有对象原型链查找的终点,也因此在Typescript中将类型声明成Object不会报错,但无法使用非对象的属性/方法

    2. object作为类型声明时,变量值只能是对象,其他值会报错。值得注意的是,object声明的对象无法访问/添加对象上的任何属性/方法,实际效果类似于通过Object.create(null)创建的空对象,暂时不知道这么设计的原因

    3. {}其实就是匿名形式的type,因此支持通过&|操作符对类型声明进行扩展(即交叉类型和联合类型)

    // 赋值给数字不会报错
    let one: Object = 1
    // 也赋值给数组,但无法使用数组的push方法
    let arr: Object = []
    // 会报错
    arr.push(1)
    
    // 赋值会报错
    let two: object = 2
    
    // object作为类型声明时,赋值给对象时不会报错
    let obj1: object = {}
    let obj2: object = { name: '王五' } 
    let Obj3: Object = {}
    
    // 会报错
    obj1.name = '张三'
    obj1.toString()
    obj2.name
    
    // 不会报错
    Obj3.name = '李四'
    Obj3.toString()
    
    // {} 等同于匿名形式的type
    type UserType = { name: string; }
    
    let user: UserType = { name: '李四' }
    let data: { name: string; } = { name: '张三' }
    

    交叉类型和联合类型

    上文提到,Typescript支持通过&|操作符对类型声明进行扩展,用&相连的多个类型是交叉类型,用|相连的多个类型是联合类型。

    两者之间的区别主要体现在联合类型主要在做类型的合并,如Form4TypeForm6Type;而交叉类型则是求同排斥,如Form3TypeForm5Type。也可以用数学上的合集和并集来分别理解联合类型和交叉类型。

    
    type Form1Type = { name: string; } & { gender: number; }
    // 等于 type Form1Type = { name: string; gender: number; }
    type Form2Type = { name: string; } | { gender: number; }
    // 等于 type Form2Type = { name?: string; gender?: number; }
    
    let form1: Form1Type = { name: '王五' } // 提示缺少gender参数
    let form2: Form2Type = { name: '刘六' } // 验证通过
    
    
    type Form3Type = { name: string; } & { name?: string; gender: number; }
    // 等于 type Form3Type = { name: string; gender: number; }
    type Form4Type = { name: string; } | { name?: string; gender: number; }
    // 等于 type Form4Type = { name?: string; gender: number; }
    
    let form3: Form3Type = { gender: 1 } // 提示缺少name参数
    let form4: Form4Type = { gender: 1 } // 验证通过
    
    
    type Form5Type = { name: string; } & { name?: number; gender: number; }
    // 等于 type Form5Type = { name: never; gender: number; }
    type Form6Type = { name: string; } | { name?: number; gender: number; }
    // 等于 type Form6Type = { name?: string | number; gender: number; }
    
    let form5: Form5Type = { name: '张三', gender: 1 } // 提示name的类型为never,不能进行赋值
    let form6: Form6Type = { name: '张三', gender: 1 } // 验证通过
    

    上述的代码片段一般只会在面试题里面出现,如果这种代码出现在真实的项目代码里面,估计在代码评审的时候就直接被点名批评了。

    不过也不是没有实用场景,以苹果的教育优惠举个例子:假设原价购买苹果12需要5000元;如果通过教育优惠购买则可以享受一定折扣的优惠(比如打8折),但是需要提供学生证或者是教师证。经过产品经理的整理,转变为需求文档之后可能就变成了:原价购买无需其他材料,如需享受教育优惠,则需要提交个人资料以及学生证/教师证扫描件。

    // 原价购买
    type StandardPricing = {
      mode: 'standard';
    }
    // 教育优惠购买需要提供购买人姓名和相关证件
    type EducationPricing = {
      mode: 'education';
      buyer_name: string;
      sic_or_tic: string;
    }
    // 通过&和|合并类型
    type buyiPhone12 = { price: number; } & ( StandardPricing | EducationPricing )
    
    
    let standard: buyiPhone12 = { mode: 'standard', price: 5000 }
    let education: buyiPhone12 = { mode: 'education', price: 4000, buyer_name: '张三', sic_or_tic: '证件' }
    

    Type和Interface

    在一开始学习Typescript的时候看到interface,我第一时间想到的是JavaJavainterface是一种抽象类,把功能的定义和具体的实现进行分离,方便不同人员可以通过interface进行相互配合,类似于需求文档在开发中的作用。

    // 张三定义了用户中心的功能有三个:登录、注册、找回密码
    interface UserCenterDao {
      void userLogin();
      void userRegister();
      void userResetPassword();
    }
    
    // 李四开发用户中心的功能就会提示需要实现三个功能
    class UserCenter implements UserCenterDao {
      public void userLogin() {};
      public void userRegister() {};
      public void userResetPassword() {};
    }
    

    Typescript对于interface的定义也是类似,都是声明一系列的抽象变量/方法,然后通过具体的代码去实现。

    interface整体的效果与用type声明的效果非常相似,即使是专属于interface的继承extendstype也可以通过&|操作符实现,两者之间也不是独立的,也可以互相进行调用。

    因此在平时的实际开发中,不必太过纠结使用type还是interface进行类型的声明,特别纠结的时候type一把梭。

    // 用interface定义一个学生的基础属性为姓名、性别、学校、年级、班级
    interface Student {
      name: string;
      gender: '男' | '女';
      school: string;
      grade: string | number;
      class: number;
    }
    
    // 用interface继承学生的基础属性
    // 并追加定义三好学生的标准为遵守校规、乐于助人,班级前三
    interface MeritStudent extends Student {
      toeTheLine: boolean;
      helpingOther: boolean;
      topThreeInClass: boolean;
    }
    
    // 可以通过type将interface声明的类型声明到新声明上
    type StudentType = Student
    
    // interface虽然不能直接使用type声明的类型,但是可以通过继承间接使用
    interface CollageStudent extends StudentType {}
    
    // 然后声明相对应的逻辑去实现
    let xiaoming: Student = {
      name: '小明',
      gender: '男',
      school: '清华幼儿园',
      grade: '大大班',
      class: 1
    }
    
    let xiaowang: MeritStudent = {
      name: '小王',
      gender: '男',
      school: '清华幼儿园',
      grade: '大大班',
      class: 1,
      toeTheLine: true,
      helpingOther: true,
      topThreeInClass: true
    }
    
    let xiaohong: StudentType = {
      name: '小红',
      gender: '女',
      school: '朝阳小学',
      grade: 1,
      class: 1
    }
    

    说起typeinterface,有一道非常经典的Typescript面试题:typeinterface的区别在哪里?

    先说个人感受。我个人感觉typeinterface的区别主要是在语义上,type在官方文档的定义是类型别名,而interface的定义是接口。

    下面的代码可以非常明显体现其两者在语义上的区别,其实两者在语法方面的区别并不算大。

    // type可以给类型定义别名
    type StudentName = string
    
    // interface可以像Java定义一个学生的抽象类
    interface StudentInterface {
      addRecord: (subject: string, score: number, term: string) => void
    }
    
    // 等同于let name: string = '张三'
    let name: StudentName = '张三'
    
    
    // 构造函数CollageStudent获得抽象类StudentInterface的声明
    class CollageStudent implements StudentInterface {
    
      public record = []
    
      addRecord(subject, score, term) {
        this.record.push({ subject, score, term })
      }
    }
    
    // type其实也定义类似的类型声明结构,但是从语义上来说并不是抽象类
    type TeacherType = {
      subject: Array<string>
    }
    // 构造函数也可以获得type声明的类型,语法上是可以实现的
    // 但是从语义和规范的层面上来说不推荐这么写
    class CollageTeacher implements TeacherType {
    
      subject: ['数学', '体育']
    }
    

    至于标准答案,官方文档(点击此处)中给出了两者在语法上的具体区别。

    Typescript开发学习总结(附大量代码)

    泛型

    什么是泛型?简单来说,泛型就是类型声明里的变量。举个不相关但是很好理解的例子:

    Javascript在执行let num = 1这段代码的时候,Javascript的编译器会从右向左执行代码。代码执行之前,编译器并不知道变量num的数据类型是什么,执行完之后编译器便知道了变量num的数据类型为Number

    这也正好是泛型的核心:编译之前不知道是什么类型,编译之后就知道了

    // 泛型的书写形式是<T>,可以通过<T = ?>为泛型附默认值
    // 函数表达式的写法
    function typeOf<T>(arg: T): string {
        return Object.prototype.toString.call(arg).replace(/\[object (\w+)\]/, '$1').toLowerCase()
    }
    
    // 等同于typeOf<string>('Hello World')
    typeOf('Hello World')
    // 等同于typeOf<number>(123456)
    typeOf(123456)
    
    // 函数声明式的写法
    const size = <T>(args: Array<T>): number => args.length
    
    // 等同于size<number>([ 1, 2, 3 ])
    size([ 1, 2, 3 ])
    

    上述代码虽然比较简单,但是足以看出泛型的灵活性,这能让组件的复用性更高,不过可能还是不好理解泛型在实际项目中的用处。

    下面是我在现实的项目工程中使用的代码片段,代码有点长但是逻辑不复杂。代码主要是用于请求后端接口的hooks,定义了两个泛型:RequestConfigAxiosResponse,分别用于定义请求参数和返回参数的结构,代码中还运用了泛型嵌套Promise<AxiosResponse<T>>,方便对多层结构的复用。

    import axios, { AxiosRequestConfig } from 'axios'
    
    // 请求参数的结构
    interface RequestConfig<P> {
      url: string;
      method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
      data: P;
    }
    
    // 返回参数的结构
    interface AxiosResponse<T> {
      code: number;
      message?: string;
      data: T;
    }
    
    const $axios = axios.create({ baseURL: 'https://demo.com' })
    
    // 声明了两个泛型类型T和P
    // T - 返回参数的泛型,默认值为void,在无返回参数的时候不需要传类型声明
    // P - 请求参数的泛型,默认值为void,在无请求参数的时候不需要传类型声明
    // 泛型支持嵌套,如Promise<AxiosResponse<T>>即表示AxiosResponse<T>的返回值在Promise中
    const useRequest = async <T = void, P = void>(requestConfig: RequestConfig<P>): Promise<AxiosResponse<T>> => {
      
      const axiosConfig: AxiosRequestConfig = {
    
        url: requestConfig.url,
        data: requestConfig.data || {},
        method: requestConfig.method || 'GET'
      }
    
      try {
        // data中是预想中的返回参数
        const { data: response } = await $axios(axiosConfig)
        
        // 错误响应
        if( response.code !== 200 ) {
    
          return Promise.reject(response)
        }
    
        return Promise.resolve(response)
      } catch(e) {
        // 错误响应
        return Promise.reject(e)
      }
    }
    
    (async () => {
    
      interface RequestInterface {
        date: string;
      }
      interface ResponseInterface {
        weather: number;
      }
    
      // 无参数时使用,无需约束泛型
      await useRequest({ url: 'api/connect' })
      // 有参数时使用,通过泛型约束提升代码质量
      const { weather } = await useRequest<RequestInterface, ResponseInterface>({
        url: 'api/weather',
        data: { date: '2021-02-31' }
      })
    })()
    

    另外,Typescript允许类型声明调用自己,可以通过这个特性去实现类似于树形结构的需求,比较常见的就是管理系统的导航菜单了。

    // Typescript支持递归调用自身
    type TreeType = {
      label: string;
      value: string | number;
      children?: Array<TreeType>
    }
    // 因此可以借助这个特性实现树形结构
    let tree: Array<TreeType> = [
      
      { label: '首页', value: 1, children: [
        { label: '仪表盘', value: '1-1' },
        { label: '工作台', value: '1-2' },
      ] },
      { label: '进度管理', value: 2, children: [
        { label: '进度设置', value: '2-1' },
        { label: '操作记录', value: '2-2' },
      ] },
    ]
    

    起源地下载网 » Typescript开发学习总结(附大量代码)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元