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