夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 读书笔记

  • 专题分享

  • 项目实践

    • 圣诞节活动总结
    • vite踩坑之旅
    • vue3踩坑之旅
    • typescript实践
      • 迁移支付宝小程序踩坑记录
      • electron制作小程序上传工具总结
    • 框架应用

    • 前端一览
    • 项目实践
    frost
    2022-03-31

    typescript实践

    typescript 中文文档 (opens new window)

    深入理解 TypeScript (opens new window)

    TypeScript 体系调研报告 (opens new window)

    # 变量声明

    let isDone: boolean = false
    
    let decimal: number = 6
    
    let color: string = 'blue'
    
    // 数组,有两种写法
    let list: number[] = [1, 2, 3]
    let list: Array<number> = [1, 2, 3]
    
    // 元组(Tuple)
    let x: [string, number] = ['hello', 10]
    
    // 枚举
    enum Color {
      Red = 1,
      Green = 2,
      Blue = 4,
    }
    let c: Color = Color.Green
    
    // 不确定的可以声明为any
    let notSure: any = 4
    
    // 声明没有返回值
    function warnUser(): void {
      alert('This is my warning message')
    }
    
    let u: undefined = undefined
    
    let n: null = null
    
    // 类型永远没返回
    function error(message: string): never {
      throw new Error(message)
    }
    
    // 类型主张,就是知道的比编译器多,主动告诉编译器更多信息,有两种写法
    let someValue: any = 'this is a string'
    let strLength: number = (<string>someValue).length
    let strLength: number = (someValue as string).length
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42

    实际上,ts 会在初始化的时候根据初始值推断类型,不一定要定义。如

    let count = 4
    console.log(count.length) // Property 'length' does not exist on type 'number'.
    
    1
    2

    # 接口

    接口可以约束变量的结构。

    interface SystemConfig {
      attr1: number
      attr2: string
      func1(): string
    }
    const config: SystemConfig = {
      attr1: 1,
      attr2: 'str',
      func1: () => '',
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    注意,接口中的函数有多种写法,且参数必须写全。

    interface Fn3 {
      (num1?: number, num2?: number): number
    }
    interface Item {
      fn1: () => number
      fn2(): string
      fn3: Fn3
    }
    const item: Item = {
      fn1() {
        return 1
      },
      fn2() {
        return '2'
      },
      // fn3(num1 = 0, num2 = 0) {
      //   return num1 + num2
      // }
      fn3(num1 = 0) {
        return num1 + 1
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    Robot 类继承了 Base 类,并实现了 Machine 和 Human 接口。

    interface Machine {
      move(): void
    }
    
    interface Human {
      run(): void
    }
    
    class Base {}
    
    class Robot extends Base implements Machine, Human {
      run() {
        console.log('run')
      }
      move() {
        console.log('move')
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # type

    TS 中 type 和 interface 的区别 (opens new window)

    type 和 interface 都可以用来声明类型,但有区别。

    1. 声明类型范围

    interface 主要用于类型检查,约束结构,可以声明对象,类,函数。

    而 type 可以声明任何类型,比如基础类型,联合类型,元组等。

    // 基本类型
    type count = number
    
    // 联合类型
    interface Dog {
      name: string
    }
    interface Cat {
      age: number
    }
    type animal = Dog | Cat
    
    // 元组
    type pet = [Dog, Cat]
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    1. 扩展性方面

      接口可以 extends、implements,从而扩展多个接口或类。

      • interface extends interface
      interface Person {
        name: string
      }
      interface User extends Person {
        age: number
      }
      
      1
      2
      3
      4
      5
      6
      • interface extends type
      type Person = { name: string }
      interface User extends Person {
        age: number
      }
      
      1
      2
      3
      4

      type 没有扩展功能,只能交叉合并。

      • type & type
      type Person = { name: string }
      type User = Person & { age: number }
      
      1
      2
      • type & interface
      interface Person {
        name: string
      }
      type User = { age: number } & Person
      
      1
      2
      3
      4
    2. 同名表现

      定义两个同名的接口会合并声明。

      interface Person {
        name: string
      }
      interface Person {
        age: number
      }
      
      1
      2
      3
      4
      5
      6

      定义两个同名的 type 会出现异常

      type User = {
        name: string
      }
      type User = {
        age: number
      }
      
      1
      2
      3
      4
      5
      6
    3. type 可以获取 typeof 返回的值作为类型

      let div = document.createElement('div')
      type B = typeof div // HTMLDivElement
      
      1
      2

    # 联合类型与类型保护

    常见联合类型用法如下

    function formatCommandline(command: string[] | string) {
      let line = ''
      if (typeof command === 'string') {
        line = command.trim()
      } else {
        line = command.join(' ').trim()
      }
    
      // Do stuff with line: string
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    面对联合类型,在数据处理过程中,就必须对数据类型进行判别,才能使用。这就用到类型保护。有下面几种方式:typeof,instanceof, in, 使用字面量类型, 自定义类型。通俗地说,就是 ts 利用 js 的机制或 ts 的语法对类型进行判断,并确认是对应类型后,进行编译。

    类型保护 (opens new window)

    # typeof

    function doSome(x: number | string) {
      if (typeof x === 'string') {
        // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
        console.log(x.subtr(1)) // Error: 'subtr' 方法并没有存在于 `string` 上
        console.log(x.substr(1)) // ok
      }
    
      x.substr(1) // Error: 无法保证 `x` 是 `string` 类型
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # instanceof

    class Foo {
      foo = 123
      common = '123'
    }
    
    class Bar {
      bar = 123
      common = '123'
    }
    
    function doStuff(arg: Foo | Bar) {
      if (arg instanceof Foo) {
        console.log(arg.foo) // ok
        console.log(arg.bar) // Error
      }
      if (arg instanceof Bar) {
        console.log(arg.foo) // Error
        console.log(arg.bar) // ok
      }
      console.log(arg.common) // ok
    }
    
    doStuff(new Foo())
    doStuff(new Bar())
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # 自定义类型

    如果是自定义的接口,需要用到is。

    interface Foo {
      foo: number
      common: string
    }
    
    interface Bar {
      bar: number
      common: string
    }
    
    // 用户自己定义的类型保护!
    function isFoo(arg: Foo | Bar): arg is Foo {
      return (arg as Foo).foo !== undefined
    }
    
    // 用户自己定义的类型保护使用用例:
    function doStuff(arg: Foo | Bar) {
      if (isFoo(arg)) {
        console.log(arg.foo) // ok
        console.log(arg.bar) // Error
      } else {
        console.log(arg.foo) // Error
        console.log(arg.bar) // ok
      }
    }
    
    doStuff({ foo: 123, common: '123' })
    doStuff({ bar: 123, common: '123' })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    基于此,写一个自己遇到的:

    按钮组件对外提供一个配置,结构如下:

    ;[
      {
        text: '新增',
        click: () => {
          console.log('新增')
          add()
        },
        attr: {
          type: 'success',
        },
      },
      [
        {
          text: '下拉',
          click: () => {
            console.log('下拉')
          },
          children: [
            {
              text: '下拉1',
              click: (item, index) => {
                console.log('下拉1', item, index)
              },
            },
            {
              text: '下拉2',
              click: () => {
                console.log('下拉2')
              },
            },
          ],
        },
      ],
    ]
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

    可以看到,数组中有可能是 obj,也可能是数组包裹的 obj,那么在渲染的时候,就要判断是否数组。

    types 如下:

    import { ElButton } from 'element-plus'
    
    export interface ConfigObj {
      text?: string
      click?: (data1?, data2?) => void
      attr?: typeof ElButton | any
      children?: ConfigObj[]
    }
    
    export type ConfigItem = ConfigObj | ConfigObj[]
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    组件如下:

    <template>
      <template v-for="(item, index) of config" :key="index">
        <template v-if="isArray(item)">
          <template v-for="(btnItem, btnIndex) of item" :key="btnIndex">
            <OButton v-bind="btnItem.attr || $attrs" :item="btnItem" @click="tapBtn(btnItem.click)">
              {{ btnItem.text }}
            </OButton>
          </template>
        </template>
        <OButton v-else v-bind="item.attr || $attrs" :item="item" @click="tapBtn(item.click)">{{ item.text }}</OButton>
        <el-divider v-if="index !== config.length - 1" direction="vertical" />
      </template>
    </template>
    <script setup lang="ts">
    import { PropType } from 'vue'
    import OButton from './OButton.vue'
    import { ConfigObj, ConfigItem } from './types'
    
    const attrs = useAttrs()
    const props = defineProps({
      config: {
        type: Array as PropType<ConfigItem[]>,
        default: () => [],
      },
    })
    
    // 类型保护,否则ts会报错
    const isArray = (item: ConfigItem): item is ConfigObj[] => {
      return Array.isArray(item)
    }
    const tapBtn = (click) => {
      typeof click === 'function' && click()
    }
    </script>
    <style lang="scss" scoped></style>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    # 枚举

    收集所有可能的值。

    enum Color {
      Red,
      Green,
      Blue,
    }
    
    let col = Color.Red
    console.log(col) // 0
    
    // 也可以是字符串
    enum Color {
      Red = 'red',
      Green = 'green',
      Blue = 'blue',
    }
    
    let col = Color.Red
    console.log(col) // red
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 泛型

    泛型用于给成员之间提供有意义的约束,它是一种任意类型,但一旦确定,就明确类型了。

    泛型一般用于偏底层的类型约束,比较抽象。

    举例说明。

    改造前:

    class QueueNumber {
      private data = []
      push = (item: number) => this.data.push(item)
      pop = (): number => this.data.shift()
    }
    
    const queue = new QueueNumber()
    
    queue.push(0)
    queue.push('1') // Error: 不能推入一个 `string` 类型,只能是 `number` 类型
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    改造后:

    class Queue<T> {
      private data: T[] = []
      push = (item: T) => this.data.push(item)
      pop = (): T | undefined => this.data.shift()
    }
    
    const queue = new Queue<number>()
    
    queue.push(0)
    queue.push('1') // Error: 不能推入一个 `string` 类型,只能是 `number` 类型
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    这样一来,扩展性就很好。如果要限制 string 类型,只需要将number改为string即可。

    再看一个例子。

    function add(x: string, y: string): string
    function add(x: number, y: number): number
    
    function add(x: string | number, y: string | number): string | number {
      if (typeof x === 'string' && typeof y === 'string') {
        return x + y
      } else if (typeof x === 'number' && typeof y === 'number') {
        return x + y
      }
    }
    
    console.log(add(1, 2)) // 3
    console.log(add('hello', ' world')) // 'hello world'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    上面的代码用到了函数重载,即函数名相同,参数个数或类型不同。这里可以使用泛型替代:

    // function add(x: string, y: string): string
    // function add(x: number, y: number): number
    function add<T>(x: T, y: T): T
    
    1
    2
    3

    泛型不仅可以应用于类和函数,也可以用在接口。再看一个例子

    // 使用场景:
    // 有submit1和submit2 两个函数,他们的入参结构非常相似 都有value属性
    // 区别在于:submit1的value属性是number submit1的value属性是string
    interface SubmitData1 {
      value: number
    }
    interface SubmitData2 {
      value: string
    }
    function submit1(data: SubmitData1) {}
    function submit2(data: SubmitData2) {}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    这 2 个接口结构非常相似,于是可以优化成:

    interface SubmitData<T> {
      value: T
    }
    
    function submit1(data: SubmitData<number>) {}
    function submit2(data: SubmitData<string>) {}
    
    1
    2
    3
    4
    5
    6

    # 命名空间

    模块复杂时,可以使用 namespace。

    namespace N {
      export namespace NN {
        export function a() {
          console.log('N.a')
        }
        export interface B {
          name: string
        }
      }
    }
    
    N.NN.a()
    
    const data = {
      name: 'a',
    }
    function namespaceFn(data: N.NN.B) {}
    namespaceFn(data)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 感叹号(!)和问号(?)

    感叹号用于告诉 typescript 编译器,对象一定存在,不需要提示警告(慎用)。

    全称是非空断言操作符 (opens new window),官方示例如下。

    interface Entity {
      name: string
    }
    
    // Compiled with --strictNullChecks
    function validateEntity(e?: Entity) {
      // Throw exception if e is null or invalid entity
    }
    function processEntity(e?: Entity) {
      validateEntity(e)
      let s = e!.name // Assert that e is non-null and access name
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    问号则用于对象属性的可选链式调用,它是 es6 的特性。

    const obj = {
      hh: 'xxx',
    }
    let res = obj?.data?.list
    
    // 等价于
    let res = obj && obj.data && obj.data.list
    
    1
    2
    3
    4
    5
    6
    7

    # 未知属性的对象

    如果一个对象的属性和类型都不确定,可以使用下面的接口定义:

    interface ObjectTy {
      [x: string]: unknown
    }
    
    1
    2
    3

    # tsconfig.json

    tsconfig.json 存在于项目的根目录,用于配置 ts 编译选项,包括需要编译的文件和编译方式。

    {
      "compilerOptions": {
        /* 基本选项 */
        "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
        "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
        "lib": [], // 指定要包含在编译中的库文件
        "allowJs": true, // 允许编译 javascript 文件
        "checkJs": true, // 报告 javascript 文件中的错误
        "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
        "declaration": true, // 生成相应的 '.d.ts' 文件
        "sourceMap": true, // 生成相应的 '.map' 文件
        "outFile": "./", // 将输出文件合并为一个文件
        "outDir": "./", // 指定输出目录
        "rootDir": "./", // 用来控制输出目录结构 --outDir.
        "removeComments": true, // 删除编译后的所有的注释
        "noEmit": true, // 不生成输出文件
        "importHelpers": true, // 从 tslib 导入辅助工具函数
        "isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).
    
        /* 严格的类型检查选项 */
        "strict": true, // 启用所有严格类型检查选项
        "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
        "strictNullChecks": true, // 启用严格的 null 检查
        "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
        "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
    
        /* 额外的检查 */
        "noUnusedLocals": true, // 有未使用的变量时,抛出错误
        "noUnusedParameters": true, // 有未使用的参数时,抛出错误
        "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
        "noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
    
        /* 模块解析选项 */
        "moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
        "baseUrl": "./", // 用于解析非相对模块名称的基目录
        "paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
        "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
        "typeRoots": [], // 包含类型声明的文件列表
        "types": [], // 需要包含的类型声明文件名列表
        "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
    
        /* Source Map Options */
        "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
        "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
        "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
        "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
    
        /* 其他选项 */
        "experimentalDecorators": true, // 启用装饰器
        "emitDecoratorMetadata": true // 为装饰器提供元数据的支持
      },
      // 显示指定需要编译的文件
      "files": ["./some/file.ts"],
      // 指定需要包含的文件
      "include": ["./folder"],
      // 指定需要排除的文件
      "exclude": ["./folder/**/*.spec.ts", "./folder/someSubFolder"]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58

    下面是基于 vite 模板的精简版本:

    {
      "compilerOptions": {
        "target": "esnext",
        "useDefineForClassFields": true,
        "module": "esnext",
        "moduleResolution": "node",
        "strict": true,
        "jsx": "preserve",
        "sourceMap": true,
        "resolveJsonModule": true,
        "esModuleInterop": true,
        "lib": ["esnext", "dom"],
        "paths": {
          "@/*": ["src/*"],
          "~/*": ["typings/*"]
        },
        "types": ["element-plus/global"]
      },
      "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    # .d.ts和declare

    declare 表示声明,通常可以声明类型、变量和模块,需要在tsconfig.json的 include 配置声明所在的文件,支持 glob 通配符。声明之后,其他地方就可以不用 import 了,直 接使用。同理,.d.ts文件表示里面是声明的集合。

    一句话解释 delcare:

    declare 就是告诉 TS 编译器,你担保这些变量和模块存在,并声明了相应类型,编译的时候不需要提示错误。

    注意:

    1.使用时不需要引入,但是要确保该文件在src目录下。

    2.不要在.d.ts文件中使用export导出,否则其他顶级声明会失效。

    举例:

    // tsconfig.json
        "include": ["src/**/*.ts", "src/**/*.d.ts","typings"]
    
    1
    2
    // typings/test.d.ts
    declare interface Person1 {
        name: string
    }
    
    type Person2 = {
        name: string
    }
    
    declare const a_a = 'abc'
    
    declare namespace MyName {
        interface ABC {
            name: string
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // src/index.ts
    
    const test1 = (p: Person1) => {
        console.log(p)
    }
    const test2 = (p: Person2) => {
        console.log(p)
    }
    
    console.log(a_a)
    
    const laugh = (person: MyName.ABC) => {
        console.log(person)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    ts的.d.ts和declare究竟是干嘛用的 (opens new window)

    # 推荐插件:

    # 根据接口数据快速生成接口

    JSON to TS

    与后端联调的时候,请求参数和返回字段很多的时候,一个个录入 ts 类型是非常痛苦的事。有了它,可以直接复制 json 数据,然后新建一个空文件,shift + ctrl + P,找 到JSON to TS点击即可生成 ts 类型。

    # 建议:

    • 类型断言需谨慎。因为使用断言(as)后,ts 就不会报错了,等于失去了这部分警告提示。
    • 尽量不使用 any 类型,如果确实不知道类型,可以使用 unkown 代替。
    #最佳实践
    上次更新: 2023/06/16, 18:10:25
    vue3踩坑之旅
    迁移支付宝小程序踩坑记录

    ← vue3踩坑之旅 迁移支付宝小程序踩坑记录→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×