前端知识分享——TypeScript 学习记录

TypeScript 学习记录

  • TypeScript 学习记录
    • 快速上手
    • 配置文件
    • 原始类型
    • 标准库声明
    • 中文错误消息
    • 作用域问题
    • TypeScript Object 类型
    • TypeScript 数组类型
    • TypeScript 元组类型
    • TypeScript 枚举类型
    • TypeScript 函数类型
    • TypeScript 任意类型
    • TypeScript 隐式类型推断
    • TypeScript 类型断言
    • TypeScript 接口(Interfaces)
    • TypeScript 接口补充
    • TypeScript 类的基本使用
    • TypeScript 类的访问修饰符
    • TypeScript 类的只读属性
    • TypeScript 类与接口
    • TypeScript 抽象类
    • TypeScript 泛型
    • TypeScript 类型声明

TypeScript 学习记录

Write and share what I see and hear

点击访问本文代码地址

快速上手

  • 初始化项目
    yarn init -y
  • 安装 typescript
    • 可全局安装,此处安装为开发依赖
      yarn add typescript --dev
  • 编写 .ts 文件
      // 01-getting-started.ts
      const hello = (name: string) => {
        console.log(`Hello ,${name}`);
      }
    
      hello('TypeScript')
    
  • 编译 .ts 文件
    yarn tsc 01-getting-started.ts

配置文件

  • 生成配置文件
    执行 yarn tsc -init 生成配置文件 tsconfig.json
  • 配置文件示例
    tsconfig.json
      {
        "compilerOptions":{
          "target": "es5", // 编译后的es版本 eg: es5  es2015
          "module": "commonjs", // 使用模块规范
          "rootDir": "src", // 编译入口文件夹
          "sourceMap": true, // 是否开启sourceMap
          "outDir": "dist", // 编译后的出口文件夹
          "strict": true, // 是否开启严格模式,开启后需要为每个变量设置类型, any 类型也需要添加
        }
      }
    
  • 新建 src 文件夹,将 .ts 文件添加到该文件夹下,进行编译
    yarn tsc

注意:配置文件 tsconfig.json 只对指定入口文件夹下的文件进行编译

原始类型

  const a: string = 'foobar'

  const b: number = 100 // 包括 NaN Infinity

  const c: boolean = true // false

  // const d: string = null

TypeScript 中以上三种类型在非严格模式下默认允许为空,即可以赋值为 null undefined

  const e: void = undefined // 通值,一般在函数没有返回值的时候标记函数的类型,只能存放 null undefined,严格模式下只能是 undefined

  const f: null = null

  const g: undefined = undefined

标准库声明

  • ES2015中新增的 symbol 类型的值,直接使用会提示错误

    • ts 有内置类型,配置文件中的 target 属性,其实已经指定了 对应的配置文件
    • target配置 es5,即指定的其实是 es5 的内置类型, symbol 类型是 es2015 中的,不在 es5 中,类似的 Promise 也不在 es5 中
  • 解决方案

    • 1.target 使用 es2015
    • 2.target 使用 es5 , 配置 lib 指定引入的标准库 “ES2015”
  • 使用方案 2 时, console 会报错,原理同 symbol 报错一样,需要在lib中引入 BOM 和 DOM 标准库----“DOM”,“DOM” 包含 BOM 和 DOM

      const h: symbol = Symbol()
    

中文错误消息

  • TypeScript 本身支持多语言化的错误消息,默认会根据开发系统和开发工具语言的设置选择报错的语言
    • 如果使用开发工具使用的是英文版的,需要报错提示为中文版本
    • 使用 tsc 命令时添加参数 --locale zh-CN
  • vscode中的错误消息可以在vscode的配置选项中进行配置
    • 设置中搜索 typescript locale ,配置TypeScript: locale 为 zh_CN

不推荐使用中文错误,不利于搜索问题

作用域问题

  • 不同文件中有相同变量名,在同一作用域中会报错
    • 创建单独作用域,例如使用立即执行函数
      (function () {
        const a: number = 123
      })()
    
    • 使用 ESModule 导出变量,此时文件中的变量就会变成局部作用域的变量
      export {}
    

export 只是 ESModule 的语法,并不是导出了一个空对象

TypeScript Object 类型

  • TypeScript 中的 Object 并不单指普通的对象类型,泛指所有的非原始类型,对象、数组、函数

      export {} // 确保与其他示例没有冲突
    
      const foo: object = function () {} // [] // {}
    
  • 如果只要普通对象类型,需要使用类似字面量语法,标记类型

      const obj: { foo: number } = { foo: 123 }
    
  • 标记多个类型,可以用逗号分隔
    这里要求赋值的类型结构必须与定义的类型结构完全一致,不能多或少

      const obj2: { foo: number, bar: string } = { foo: 123, bar: 'string' }
    

限制对象应该使用接口

TypeScript 数组类型

  • 定义数组的方式

    • 使用 Array 泛型
      const arr1: Array<number> = [1, 2, 3]
    
    • 使用元素类型 + []
      const arr2: number[] = [1, 2, 3]
    
  • 示例

      function sum (...args: number[]) {
        // 判断是不是每个成员都是数字
        return args.reduce((prev, current) => prev + current, 0)
      }
    
      // 调用时使用非数字数组会报错
      sum(1, 2 , 3, 'foo')
    

TypeScript 元组类型

元组类型是一种特殊的数据结构

  • 元组其实是一个明确元素数量及元素类型的数组

    • 各个元素的类型不必完全相同,在 ts 中可以使用类似数组字面量的语法定义元组类型
      const tuple: [number, string] = [1, 'string']
    
  • 访问元组中的某个元素

    • 使用数组下标的方式访问
      const age = tuple[0]
      const name = tuple[1]
    
    • 使用数组结构的方式访问
      const [age, name] = tuple
    
  • 元组一般用在一个函数中返回多个返回值,这种类型现在越来越常见
    • react 中 useState 的 HOOK函数中返回的就是一个元组
    • es2017 中 Object.entries 方法,获取一个对象中所有的键值数组,得到的每一个键值,就是一个元组,因为它是一个固定长度的

TypeScript 枚举类型

枚举(Enum)

  • 枚举的特点
    • 为一组数值定义更容易理解的名称
    • 一个枚举中只会存在几个固定的值,不会出现超出范围的可能性
  • 枚举在其他语言中是很常见的数据结构,但是在 js 中没有这种类型,很多时候都是通过对象模拟实现枚举
      const PostStatus = {
        Draft: 0,
        Unpublished: 1,
        Published: 2
      }
      const post = {
        title: 'title',
        content: 'content',
        status: PostStatus.Draft
      }
    
  • ts 中有专门的枚举类型
      enum PostStatus {
        Draft = 0,
        Unpublished = 1,
        Published = 2
      }
      const post = {
        title: 'title',
        content: 'content',
        status: PostStatus.Draft
      }
    

枚举类型的值可以不用 = 指定,如果不指定默认从0累加,如果给第一个设置了值,后面的依次累加
除了可以设置数字,还可以是字符串,字符串无法自增长,所以字符串需要手动为每个类型赋值

  • 注意:
  • 枚举类型会入侵到运行时的代码,即会影响编译后的结果
    • 我们在 TypeScript 中使用的大多数类型经过编译转换后都会被移除,这些类型只是为了在编译过程中做类型检查
  • 枚举不会被移除,而是会被编译为一个双向的键值对对象
    • 双向键值对,就是可以通过键获取值,也可以通过值获取键
    • 这样做的目的其实是为了可以动态的通过枚举的值获取到键
      PostStatus[0] // Draft
    
    • 如果确认代码中不会使用索引器的方式访问枚举,建议使用常量枚举
      const enum PostStatus {
        Draft = 0,
        Unpublished = 1,
        Published = 2
      }
    

TypeScript 函数类型

函数的类型约束,即是指对函数的输入输出进行类型限制

  • 函数声明

    • 入参的类型注解添加在每个入参的后面,出参类型在入参的括号后进行添加
      function func1 (a: number, b: number): string {
        return ''
      }
    
    • 调用函数时,参数的个数也必须完全相同,即形参和实参必须完全一致
      func1(100, 200)
    
    • 如果需要某个参数是可选的,可以使用可选参数的特性,也可以使用 es6 新增的参数默认值属性
      function func1 (a: number, b?: number): string {
        return ''
      }
    
      function func2 (a: number, b: number = 10): string {
        return ''
      }
    

    无论是可选参数,还是默认参数,都需要出现在参数列表的最后,因为参数会按照位置进行传递

    • 如果需要使用任意参数个数,可以使用 es6 的 …rest 操作符
      function func2 (a: number, b: number = 10, ...rest): string {
        return ''
      }
    
  • 函数表达式

    • 这里的函数表达式最终是存入一个变量中,接收这个函数的变量也应该有类型
    • 一般情况下,TypeScript 可以根据函数表达式推断出这个变量的类型
    • 如果把一个函数作为参数传递(回调函数),这种情况下必须要对这个形参的类型进行约束,这种情况下,可以使用箭头函数的形式表示这个参数可以接受什么样的函数
      const func2 = function func2 (a: number, b: number): string {
        return ''
      }
    

TypeScript 任意类型

  • 由于 JS 是弱类型语言,很多内置的 API 本身就支持任意类型的参数
  • TS 是基于 JS 类型之上的,使用过程中难免遇到需要任意类型的参数
  • 此时可以使用 any 类型
    • any 类型仍然属于动态类型
    • 它的特点与普通的 JS 变量一样,即可以接收任意类型的值,在运行过程中还能接收其他类型的值
      function stringify (value: any) {
        return JSON.stringify(value)
      }
      stringify('string')
      stringify(100)
      stringify(true)
    
    • 由于 any 有可能会存放任意类型的值,所以 TypeScript 不会对 any 做类型检查,这也就意味着我们可以像在 JS 中一样去调用它任意的成员,语法上都不会报错,any 仍然会存在类型安全的问题
    • 所以不要轻易使用 any 类型
    • 兼容老代码的时候,可能难免使用 any 类型
      let foo: any = 'string'
      foo = 100
      foo.bar()
    

TypeScript 隐式类型推断

在 TypeScript 中,如果我们没有通过类型注解明确标记一个变量的类型,TypeScript 会根据这个变量的使用情况,推断这个变量类型,这个特性叫做隐式类型推断

  let age = 18 // 此时 age 被推断为 number 类型
  age = 'string' // 报错
  • 如果 TypeScript 无法推断某个变量的类型,这个变量会被标记为 any
      let foo // 声明变量未赋值,会被标记为 any 类型
      foo = 100
      foo = 'string'
    

虽然 TypeScript 支持隐式类型推断,而且这种隐式类型推断可以简化一部分代码,这里仍然建议为每个变量尽可能的添加明确的类型,便于后期更直观的理解代码

TypeScript 类型断言

  • 有些特殊情况下 TypeScript 无法推断出变量的具体类型,作为开发者,我们应该明确知道这个变量是什么类型的
      // 假定这个 nums 辣子一个明确的接口
      let nums = [110, 120, 130, 112]
      // 此时调用 find 方法获取第一个大于0的数字是可以获取到的,
      const res = nums.find(i => i > 0)
    
    • 但是对于 TypeScript 来说,它并不知道,它所推断出来的 res 类型是一个 number | undefined,因为它会认为有可能找不到
    • 此时是不能把 res 当作一个数字使用的
      const square = res * res // 报错
    
    • 这种情况下,可以断言这个 res 是number类型,断言其实就是告诉 TypeScript 此时 res 是 number 类型的
    • 类型断言的方式有两种
      • 使用 as 关键字(推荐使用
        const num = res as number
      
      • 在变量前使用 <> 断言(jsx 下不能使用
        const num = <number>res
      

      如果在代码中使用 jsx,这里的尖括号会和 jsx 中的标签产生冲突,在 jsx 下不能使用这种断言方式

    类型断言并不是类型转换,编译过后这个断言就不会存在了

TypeScript 接口(Interfaces)

  • 接口,可以理解为一种规范/契约,是一个抽象的概念,可以约定对象的结构
  • 使用某个接口,就必须要遵循这个接口全部的约定
    • 在 TypeScript 中,接口最直观的体现就是,约定一个对象中具体应该有哪些成员,这些成员的类型应该是什么样的
      // 此函数入参隐性要求必须要有 title, content 属性
      // 可以使用接口表现这种约束
      function printPost (post) {
        console.log(post.title);
        console.log(post.content);
      }
    
  • 定义接口
      interface Post {
        title: string // 可以使用 , 分隔,更标准的语法是 ; 分隔,这个分号也可以省略
        content: string
      }
      // 此函数入参隐性要求必须要有 title, content 属性
      // 可以使用接口表现这种约束
      function printPost (post: Post) {
        console.log(post.title);
        console.log(post.content);
      }
    
      printPost({
        title: 'title',
        content: 'content'
      })
    

    TypeScript 中的接口是为了给有结构的数据做约束的,在实际运行阶段,这种接口并没有意义,编译后不会体现

TypeScript 接口补充

  • 可选成员

      interface Post {
        title: string // 可以使用 , 分隔,更标准的语法是 ; 分隔,这个分号也可以省略
        content: string
        subtitle?: string // 可选成员 相当于给 subtitle 标记类型为 string 或 undefined
      }
    
  • 只读成员

      interface Post {
        title: string // 可以使用 , 分隔,更标准的语法是 ; 分隔,这个分号也可以省略
        content: string
        subtitle?: string // 可选成员 相当于给 subtitle 标记类型为 string 或 undefined
        readonly summary: string // 只读成员
      }
    
      printPost({
        title: 'title',
        content: 'content',
        summary: 'summary'
      })
    
  • 动态成员

      interface Cache1 {
        [prop: string]: string
      }
    
      const cache: Cache1 = {}
    
      cache.foo = 'value1'
      cache.bar = 'value2'
    

TypeScript 类的基本使用

类,描述一类具体事物/对象的抽象特征

  • JavaScript

    • ES6 以前都是 函数 + 模型 实现类
    • ES6 开始 JavaScript 中有了专门的 class
  • TypeScript

    • 可以使用所有 ECMAScript 标准中所有关于类的功能
    • 增强了一些类的属性
  • 类的属性在使用前必须要声明,为了给属性做类型标注

      class Person {
        name: string // = 'init' // 指定类的属性,可以使用 = 赋初始值
        age: number
        // TypeScript 中类的属性必须要有一个初始值,可以使用 = 指定,也可以在构造函数 constructor 中初始化,否则会报错
        constructor (name: string, age: number) {
          this.name = name
          this.age = age
        }
    
        sayHi (msg: string): void {
          console.log(`I am ${this.name}, ${msg}`);
        }
      }
    

TypeScript 类的访问修饰符

  • public 公有成员 默认 有无 public 均可,建议添加

  • private 私有属性 属性前添加 private 表示私有属性,只能内部访问]

  • protected 受保护的属性 属性前添加 protected 表示受保护的属性,只能内部访问]

      class Person {
        public name: string
        private age: number
        protected gender: boolean
    
        constructor (name: string, age: number) {
          this.name = name
          this.age = age
          this.gender = true
        }
    
        sayHi (msg: string): void {
          console.log(`I am ${this.name}, ${msg}`);
          console.log(this.age);
        }
      }
    
      const tom = new Person('tom', 18)
      console.log(tom.name)
      // console.log(tom.age) // 报错
      // console.log(tom.gender) // 报错
    
  • private 与 protected 的区别

    • protected 允许在子类中访问
      class Student extends Person {
        constructor (name: string, age: number) {
          super(name, age)
          console.log(this.gender);
        }
      }
    
  • 构造函数 constructor 的访问修饰符默认为 public

  • 如果设置为 private,那这个类型就不能在外部被实例化,也不能被继承,此时,只能这个类的内部创建静态方法,在静态方法中创建这个类的实例

      class Student extends Person {
        private constructor (name: string, age: number) {
          super(name, age)
          console.log(this.gender);
        }
        static create (name: string, age: number) {
          return new Student(name, age)
        }
      }
    
      const jack = Student.create('jack', 18)
    
  • 如果设置为 protected,那这个类型就不能在外部被实例化,但可以被继承

TypeScript 类的只读属性

  • readonly 只读,如果属性已有访问修饰符,readonly 跟在访问修饰符后面
  • 一个属性如果有 readonly 标识,只能使用 = 赋初始值,或者在构造函数中赋值,两者只能选其一
  • 初始化后 readonly 属性就不能再次被修改了
      class Person {
        public name: string
        private age: number
        protected readonly gender: boolean
    
        constructor (name: string, age: number) {
          this.name = name
          this.age = age
          this.gender = true
        }
    
        sayHi (msg: string): void {
          console.log(`I am ${this.name}, ${msg}`);
          console.log(this.age);
        }
      }
    
      const tom = new Person('tom', 18)
      console.log(tom.name)
      tom.gender = false // 报错
    

TypeScript 类与接口

不同的类之间可能会有共同的特征,这些不同的类的共同特征可以使用接口进行抽象
例如: 手机和座机 都能打电话,可以看作它们拥有相同的协议(接口)

  // 此处 人和动物 都有 吃和跑 的方法,但是 人与动物 的方法是不同的,只是都有这个能力,但能力的实现是不同的
  class Person {
    eat (food: string): void {
      console.log(`Person eat: ${food}`);
    }
    run (distance: number): void {
      console.log(`Person run: ${distance}`);
    }
  }

  class Animal {
    eat (food: string): void {
      console.log(`Animal eat: ${food}`);
    }
    run (distance: number): void {
      console.log(`Animal run: ${distance}`);
    }
  }
  • 定义接口
      interface EatAndRun {
        eat (food: string): void
        run (distance: number): void
      }
    
      class Person implements EatAndRun {
        eat (food: string): void {
          console.log(`Person eat: ${food}`);
        }
        run (distance: number): void {
          console.log(`Person run: ${distance}`);
        }
      }
    
      class Animal implements EatAndRun {
        eat (food: string): void {
          console.log(`Animal eat: ${food}`);
        }
        run (distance: number): void {
          console.log(`Animal run: ${distance}`);
        }
      }
    
  • 拆分接口
    • 每个类中的能力是不同的,因为使用接口就必须定义接口约定的所有属性,所以最好将接口进行拆分
      interface Eat {
        eat (food: string): void
      }
    
      interface Run {
        run (distance: number): void
      }
    
      class Person implements Eat, Run {
        eat (food: string): void {
          console.log(`Person eat: ${food}`);
        }
        run (distance: number): void {
          console.log(`Person run: ${distance}`);
        }
      }
    
      class Animal implements Eat, Run {
        eat (food: string): void {
          console.log(`Animal eat: ${food}`);
        }
        run (distance: number): void {
          console.log(`Animal run: ${distance}`);
        }
      }
    

TypeScript 抽象类

  • 抽象类可以用来约束子类中必须有某个成员
  • 与接口不同的是,抽象类可以包含一些具体的实现,接口只能是一个成员的抽象,不包含具体的实现
  • 比较大的类目建议使用抽象类,例如上面例子中的动物
      // 在 class 前添加 abstract 定义抽象类
      abstract class Animal {
        eat (food: string): void {
          console.log(`Animal eat: ${food}`);
        }
        // 抽象类中还可以定义抽象方法,抽象方法也不需要返回体
        abstract run (distance: number): void
      }
    
      // 抽象类只能被继承,不能使用 new 的方式创建实例对象,抽象类中有抽象方法,子类中就必须要实现这个抽象方法
      class Dog extends Animal {
        run(distance: number): void {
          console.log(`Dog run: ${distance}`);
        }
      }
    
      const d = new Dog()
      d.eat('food')
      d.run(100)
    

TypeScript 泛型

泛型是指在定义函数接口类的时候没有指定具体类型,等到使用时再定义类型,这样一种特征
以函数为例,泛型就是在声明函数时不指定具体类型,而是调用时传递具体的类型,这样可以极大程度的服用代码

  // 生成指定长度的数组
  function createNumberArray(length: number, value: number): number[] {
    // Array 其实是一个 any 类型,可以在使用时指定需要的类型
    const arr = Array<number>(length).fill(value)
    return arr
  }

  const res = createNumberArray(3, 100)
  // => res => [100, 100, 100]
  function createArray<T> (length: number, value: T): T[] {
    const arr = Array<T>(length).fill(value)
    return arr
  }

  const res = createArray<string>(3, '100')
  // => res => [100, 100, 100]

TypeScript 类型声明

  • 并不是所有引入的 npm 模块都是使用 TypeScript 编写的, 如果没有声明模块可以使用以下操作
  • 函数声明时未指定类型,使用时可以指定类型
      import { cameCase } from 'loadsh'
      declare function cameCase(input:string): string
    
      const res = cameCase('hello typed')
    
  • 也可以安装指定的类型声明模块
    例如 yarn add &types/lodash --dev

你可能感兴趣的:(前端知识分享,前端,typescript,javascript)