Typescript学习(十六)类型指令、类型声明、命名空间

98 阅读10分钟

前面我们学习了大量Typescript的大量类型能力, 接下来, 我们学习Typescript的工程化方面的知识, 而工程化的基础又包含了类型指令、类型声明和命名空间, 他们是我们后续学习工程化的基础知识;

类型指令

我们在Typescript实际开发中, 常常遇到一些由于Typescript无法推断或者其他一些未知问题导致的报错, 而我们同时也清楚这些问题不会影响项目的正常运行, 此时, 就可以使用类型指令来忽视掉这些报错, 所谓的类型指令, 其实就是行内注释; Typescript的行内注释统一用@ts-xx来表示, Typescript的类型指令总共有四个, 当我们在注释内写上@的时候, 就会自动提示出来:

@ts-ignore

先来看看@ts-ignore, 它的作用很单纯 即: 忽略下一行的类型检查, 这里要注意, 只是忽略下一行; 如果你下一行之后的代码还是写错了, 且其上没有@ts-ignore,则一样会报错;

// @ts-ignore
let str:string = 123 // 正常
str = 456 // 报错

但是它有个缺点, 就是'无脑'忽略, 不管下一行代码是否真的有错, 一概忽略, 正确的也给忽略了! 所以, Typescript后续推出了更加严格的类型指令@ts-expect-error

@ts-expect-error

这个类型指令我们见过, 还记得上一节模版字符串类型中, 驼峰类型工具那个案例吗?

type CapitalizeCamel<ARR extends Array<string>> =
ARR extends [infer First, ...infer Rest] ?
// @ts-expect-error
`${Capitalize<First>}${CapitalizeCamel<Rest>}`:
''

这里案例中, 由于无法推断出Rest类型, 所以我们使用了@ts-expect-error; 它的基本功能和@ts-ignore并无二致, 只是它更加严格, 那就是它修饰的那一行必须有报错, 否则它自己反而报错!

// @ts-expect-error     报错: 无用的'@ts-expect-error'指令!
let str:string = 'hello world'
// 没有'@ts-expect-error'下一行同样报错
str = 123 // 报错

注意, 在使用类型指令的时候, 你不可以在@符号前面写别的东西, 否则无效! 例如, 上面我写的那句注释: 没有'@ts-expect-error'下一行同样报错, 注意, 这句注释中的@ts-expect-error不具备类型指令能力! 但是如果我这样写, 就不会报错:

let str:string = 'hello world'
// @ts-expect-error 在类型指令后面加文字, 不影响其功能发挥
str = 123 // 不报错

@ts-nocheck

上面两种指令都是针对行的, 接下来要介绍的@ts-nocheck和@ts-check则是针对整个文件的! 先来看看@ts-nocheck

// @ts-nocheck

let num:number = 'this is string!'

let str:string = 100

let bool:boolean = function () {}

以上代码全部正常! 这就是@ts-nocheck, 整个文件都不再进行类型检查了!

@ts-check

最后的这个@ts-check也非常好理解, 就是要进行类型检查... 你可能会觉得奇怪, 这不多余吗? 我都用Typescript了, 还要特别提示需要进行类型检查? 其实这个不是给ts文件用的, 而是给Typescript工程中的js文件用的! 我们知道Javascript是弱类型语言, 所以你可以先给一个变量赋值为字符串, 再将其重新赋值为数字!

let num = 1
num = '123'

但是使用了@ts-check之后, 情况就变了

// @ts-check
/**
 * @type {string}
 */
let str = 123 // 报错

let num = 1
num = '123' // 报错

这里我们利用了jsDoc显式标注变量str必须为string类型, 被赋值为1的num也被锁定为了number类型, 不能再被重新赋予其他类型了!

类型声明

declare关键字

这里所谓的类型声明, 其实就是利用declare关键字, 来为一些变量声明类型, 姑且不论它的功能, 我们编译一段Typescript代码试试, 以下是一段很简单的Typescript代码:

// index.ts
interface Person {
  name: string;
  age: number;
}

let person:Person = {
  name: '大明',
  age: 22
}

将tsconfig.json中的declaration设置为true, 然后编译, 会生成两个文件: index.js和index.d.ts

// index.js
"use strict";
let person = {
  name: '大明',
  age: 22
};

同时, 生成.d.ts文件, 即类型声明文件

//index.d.ts
interface Person {
  name: string;
  age: number;
}
declare let person: Person;

我们可以看到, declare出现在了.d.ts类型声明文件中, 我们知道浏览器能够运行的是Javascript代码, 所以, 我们写的Typescript代码必须被编译为.js! 但是如果我们希望编译后的代码仍具备类型推导, 则需要.d.ts文件, 做类型声明, 怎么做呢? 从以上声明文件的代码可以看出, 它用declare声明了一个person变量, 并且类型是Person类型; 而这个person其实就是指代index.js中的person变量! 它的作用就是告诉用户, index.js文件中的person,它是Person类型的! 因此, 我们可以得出结论: declare + 变量类型声明代码, 仅用于表达这个变量具备什么类型, 是纯类型层面的东西; 为什么说是纯类型层面的东西呢? 我们再把index.d.ts中的代码, 复制粘贴到index.ts中, 重新编译看看

我们会发现, 啥也没有! 而正常来讲.ts文件中的声明代码,应该是会编译的, 仅仅因为前面加了一个declare, 这段代码也就完全成了一段永远不会被执行的类型声明代码! 而这种给Javascript代码声明类型的声明性代码, 让我们想到了什么? 没错, 就是第三方库中的@types/xx这种npm包, 即DefinitelyTyped(高质量TypeScript类型定义的存储库), 说白了, 就是使用类型声明, 来定义一些用javascript写成的库! 例如: @types/react、@types/lodash

类型覆盖

在实际开发中, 我们会引入各种资源, 比如: 一个npm包, 一个无类型的全局变量(如, 直接引入一段js代码), 一个非代码资源(如: 图片, markDown文档等等), 这些资源, 都有可能报出类型不明的错误, 毕竟这些内容不是我们开发的, 而这些问题, 其实都能通过declare来解决;

无类型的npm包

实际开发中, 经常会遇到一些比较老的库, 并不包含类型声明, 这会导致我们的类型系统在这部分产生缺失, 有可能因此出现问题, 所以此时我们就要用declare为其补充类型声明

前面说了declare可以为Javascript代码声明类型, 也可以为一个库声明类型, 我们接下来来看下, 它是如何给一个npm包声明类型的, 我们可以在本地安装lodash, 执行 npm i lodash, 但是却不安装@types/lodash类型声明包

import lodash from 'lodash'
lodash.hehe = 'jack'

当我们键入hehe的时候, 不会有任何提示, 当然, 真正的lodash中也不存在什么hehe属性, 但是, 我们可以通过declare module 来以假乱真给它'新增'一个属性!

// 声明模块类型
declare module 'lodash' {
  let hehe:string
}

现在, 当我们键入lodash.hehe的时候, 看看会有什么现象:

没错, typescript给出了提示, 而且这个'hehe', 还必须是string类型的, 否则会提示错误! 当然, 这里只是作为一个展示, 因为lodash已经有了对应的类型声明包@types/lodash, 这里只是假设如果我们引入了一个没有类型声明的npm包, 可以通过declare module的方式, 去弥补它缺失的类型;

全局变量

但是, 有一些包是通过运行时动态给window对象注入全局变量的, 这种情况下如何处理? 在Javascript中, 我们知道window上的属性, 我们可以不用加window.直接访问, 比如, 我们要访问window.navigator.userAgent, 完全可以直接输入navigator.userAgent, 同样的, 我们如果想为一个全局变量声明类型, 完全可以在全局直接declare声明这个变量的类型:

// 全局变量类型声明
declare const myVue:MyVue

interface MyVue {
  data: () => object
}

// 使用
const dataFn = myVue.data
const result = dataFn()

除了直接声明全局变量, 我们还能在现有的WIndow类型基础上, 再多声明一个属性, 之所以说是再多声明一个属性, 是因为Window这个类型是Typescript本身就自带的, 它本身就自带了一堆属性声明, 其类型声明就在Typescript的lib.dom.d.ts文件之下:

Window类型

Window类型是通过interface声明的, 而同名接口是会合并的, 所以我们可以这样实现多声明一个属性

// 接口声明
interface Window {
  myVue: {
    data: () => object
  }
}

// 使用
window.myVue.data()

非代码资源

如果我们在.ts文件中引入一些诸如图片, markDown等非代码资源的时候, 可能也会报错

尽管这个文件存在, 但依然会出现这个报错, 此时, 我们也可以将其声明为一个模块

declare module '*.md' {
  const raw
  export default raw
}

注意, 这里声明的模块名和前面不同, 这里使用了一个通配符加上文件后缀名;

三斜线指令

如果说, 我们正常的代码文件中, 都是使用import来导入其他模块的代码的, 那么, 声明文件中, 就是使用三斜线指令来引入其他声明文件的, 三斜线指令语法上就是三条斜杠 + 一个reference自闭合标签

/// <reference lib="es2018" />
/// <reference path="assert.d.ts" />
/// <reference type="node" />

我们可以看到, reference标签常见的属性有三种: lib、path、type, 这三种适用不同的场景:

  1. lib, 它的值表示要引入的库的名字, 而且是Typescript自带的库, Typescript自带的库指的是哪些, 其实就在node_modules/typescript/lib文件夹下:

就是这一大坨, 可以看到, 基本上就是一些常用的API的类型声明文件, 包括: dom、es等几个大类;

  1. path, 这个最好理解了, 其值就是相对路径
// index.d.ts
declare module 'jquery' {
  const name:string
}
// index.ts
/// <reference path="./index.d.ts" />
import jquery from 'jquery'

jquery.name
  1. types, 其就是为配合DefinitelyTyped, 其值就是类型声明包的名字, 例如, 在代码中使用了@types/node中的类型, 那么引入标签就是

命名空间

基本使用

所谓的命名空间, 可以看作是对代码的一种'归类', 因为有的时候, 我们的代码可能会越来越庞大, 如果没有一个命名空间进行归类, 就会显得很乱

class Dog {}

class Cat {}

class Apple {}

class Pear {}

我们可以使用namespace 关键字来创建一个命名空间

namespace Animal {
  export class Dog {}
  export class Cat {}
}

namespace Fruit {
  export class Apple {}
  export class Pear {}
}

new Animal.Dog()

注意, 命名空间就是一个小的模块, 所以, 内部的内容, 必须被导出, 所以, 需要有export将其导出;

导入须知

如果你是在一个文件中引入其他文件的namespace, 那么需要分两种情况:

  1. 你引入的是具有实际意义的运行时代码, 即可以编译并运行的代码, 那就需要在原来的namespace前也加上export导出
// util.ts
export namespace Animal {
  export class Dog {}
  export class Cat {}
}
// index.ts
import {Animal} from './util.ts'

new Animal.Dog()
  1. 如果你要引入的是另一个文件namespace中的类型代码, 则使用reference也行, 且namespace前无需添加export
// util.ts
namespace Animal {
  type color = string;
  type height = number;
}
// index.ts
/// <reference path="./modules/Animal.ts" />

let animalColor: Animal.color = 'yellow'

仅类型导入

有的时候, 我们需要导入运行时的代码模块, 有时候又要导入类型, 那么这两者如何区分? 我们可以在引入数据前加上type

// ./util/index.ts
export type age = number
export const weight = '100kg'

// ./index.ts
import { weight} from './util/index';
import type {age} from './util/index';
let myAge:age = 123
let myWeight = weight