TS 进阶 - 实际应用 01

2023-05-17 20:16:38 浏览数 (2)

# 类型检查指令

# ts-ignore 与 ts-expect-error

ts-ignore 直接禁用对下一行代码的类型检查,其本质是 ignore 而不是 disable

代码语言:javascript复制
// @ts-ignore
const a: number = 'string'

如果代码并没有问题,ignore 反而是错误了,因此引入了更严格版本的 ignore,即 ts-expect-error,它只有在下一行代码真存在错误时才能被使用,否则会报错:

代码语言:javascript复制
// @ts-expect-error
const a: number = 'string'

// @ts-expect-error
const b: number = 1 // 无意义的 ts-expect-error

建议在所有地方都不要使用 ts-ignore,对于 ignore 指令,本来就应当确保下一行真的存在报错时才使用。

# ts-check 与 ts-nocheck

ts-nocheck 可以理解为一个作用于整个文件的 ignore 指令,使用之后整个 TS 文件都不再接受类型检查:

代码语言:javascript复制
// @ts-nocheck
const a: number = 'string'
const b: string = 1

ts-check 看起来比较多余,因为默认 TS 文件都会被检查。但实际上,这两个指令还可以用在 JS 文件中。TypeScript 并不是只能检查 TS 文件,对于 JS 文件也可以通过类型推导与 JSDoc 的方式进行不完全的类型检查:

代码语言:javascript复制
// JavaScript 文件
let myAge = 18;

// JSDoc 标注类型
/** @type {string} */
let myName;

class Foo {
  prop = 599;
}

声明了初始值的 myAgeFoo.prop 都能被推导出其类型,而无初始值的 myName 也可以通过 JSDoc 标注的方式来显式标注类型。

如果希望在 JS 文件中也能享受类型检查,此时 ts-check 指令既可以登场:

代码语言:javascript复制
// @ts-check
/** @type {string} */
let myName = 18; // 报错

# 类型声明

代码语言:javascript复制
declare var f1: () => void;

declare interface Foo {
  prop: string;
}

declare function foo(input: Foo): Foo;

declare class Foo {}

可以直接访问这些声明:

代码语言:javascript复制
declare let otherProp: Foo['prop'];

但不能为这些声明变量赋值:

代码语言:javascript复制
declare let result = foo(); // 报错 Initializers are not allowed in ambient contexts.

这些类型声明就像在 TypeScript 中的类型标注一样,会存放特定的类型信息,同时由于它们并不具有实际逻辑,可以很方便使用类型声明来进行兼容性比较、工具类型的声明与测试等。

声明文件,更常见的情况是 TypeScript 代码在编译后生成声明文件:

代码语言:javascript复制
// 源代码
const handler = (input: string): boolean => {
  return input.length > 5;
}

interface Foo {
  name: string;
  age: number;
}

const foo: Foo = {
  name: 'Cell',
  age: 18,
}

class FooCls {
  prop!: string;
}

编译后会生成一个 .js 文件和一个 .d.ts 文件,后者是类型声明文件:

代码语言:javascript复制
declare const handler: (input: string) => boolean;
interface Foo {
    name: string;
    age: number;
}
declare const foo: Foo;
declare class FooCls {
    prop: string;
}

.d.ts 的核心作用是将类型独立于 .js 文件进行存储,在别人使用时,可以获得额外的类型信息。

# 让类型定义全面覆盖项目

通过额外的类型声明文件,在核心代码文件以外去提供对类型的进一步补全。

类型声明文件,即 .d.ts 文件,会自动被 TS 加载到环境中,实现对应部分代码的类型补全。

声明文件中不包含实际的代码逻辑,只做一件事:为 TypeScript 类型检查与推导提供额外的类型信息,而使用的语法仍然是 TypeScript 的 declare 关键字。

对于无类型定义的 npm 包,可以通过 declare module 来提供其类型:

代码语言:javascript复制
import foo from 'pkg';

const res = foo.handler();

添加类型提示:

代码语言:javascript复制
declare module 'pkg' {
  const handler: () => boolean;
}

可以在 declare module 中使用默认导出:

代码语言:javascript复制
declare module 'pkg2' {
  const handler: () => boolean;
  export default handler;
}

// 使用
import bar from 'pkg2';
bar();

使用类型声明还可以为非代码文件,如图片、CSS 文件等声明类型:

代码语言:javascript复制
// index.ts
import raw from './note.md';

const content = raw.replace('NOTE', `NOTE${new Date().getDay()}`);

// index.d.ts
declare module '*.md' {
  const raw: string;
  export default raw;
}

在实际使用中,如果一个库没有内置类型定义,TypeScript 会提示你,是否要安装 @types/xxx 相关的包。@types/ 开头的这一类 npm 包属于 DefinitelyTyped,它是 TypeScript 维护的,专用于为社区存在的无类型定义的 JavaScript 库添加类型支持。

三斜线指令,就像上面文件中的导入语句一样,它的作用就是声明当前的文件依赖的其他类型声明。这里的“其他类型声明”包括了 TS 内置类型声明、第三方库的类型声明以及自定义的类型声明。

代码语言:javascript复制
/// <reference path="./other.d.ts" />
/// <reference types="node" />
/// <reference lib="dom" />

注意,三斜线指令必须放置在文件的顶部才能生效。

使用 pathreference 指令,其 path 属性的值为一个相对路径,指向你项目内的其他声明文件。在编译时,TS 会沿着 path 指定的路径不断深入寻找,最深的那个没有其他依赖的声明文件会被最先加载。

使用 typesreference 指令,其 types 的值是一个包名,也就是想引入的 @types/ 声明,如 /// <reference types="node" /> 是在声明当前文件对 @types/node 的依赖。如果代码文件(.ts)中声明了对某一个包的类型导入,那再编译产生的声明文件(.d.ts)中就会自动添加对应的 reference 指令。

使用 libreference 指令,其 lib 的值是一个 TS 内置库的名字,如 /// <reference lib="dom" /> 是在声明当前文件对 DOM 的依赖。

# 命名空间

命名空间就像一个模块文件一样,将一组强相关的逻辑收拢到一个命名空间内部。

代码语言:javascript复制
export namespace RealCurrency {
  export class WeChatPaySDK {}

  export class ALiPaySDK {}

  export class UnionPaySDK {}
}

export namespace VirtualCurrency {
  export class BitCoinPaySDK {}

  export class EtherPaySDK {}
}

注意,这里的 .ts 文件中,此时它是具有实际逻辑意义的,不能和类型混作一谈。

命名空间的使用类似于枚举,命名空间内部实际上就是一个独立的代码文件,其中的变量需要导出以后,才能访问。

命名空间的作用也是实现简单的模块化功能。

命名空间内部可以嵌套命名空间,此时嵌套的命名空间也需要被导出:

代码语言:javascript复制
export namespace VirtualCurrency {
  export class QQCoinPaySDK {}

  export namespace BlockChainCurrency {
    export class BitCoinPaySDK {}

    export class EtherPaySDK {}
  }
}

类似于类型声明中的同名接口合并,命名空间也可以进行合并,但需要通过 三斜线 指令来声明导入:

代码语言:javascript复制
// animal.ts
namespace Animal {
  export namespace ProtectedAnimals {}
}

// dog.ts
/// <reference path="animal.ts" />
namespace Animal {
  export namespace Dog {
    export function bark() {}
  }
}

// corgi.ts
/// <reference path="dog.ts" />
namespace Animal {
  export namespace Dog {
    export namespace Corgi {
      export function corgiBark() {}
    }
  }
}

实际使用是需要导入全部依赖:

代码语言:javascript复制
/// <reference path="animal.ts" />
/// <reference path="dog.ts" />
/// <reference path="corgi.ts" />

Animal.Dog.Corgi.corgiBark();

命名空间也可以在声明文件中使用,即 declare namespace:

代码语言:javascript复制
declare namespace Animal {
  export interface Dog {}

  export interface Cat {}
}

declare let dog: Animal.Dog;
declare let cat: Animal.Cat;

@types/ 系列的包下,想通过 namespace 进行模块的声明,还需要注意将其导出,然后才会加载到对应的模块下:

代码语言:javascript复制
export = React;
export as namespace React;
declare namespace React {
  // ...
  function useState<T>(initialState: T | (() => T)): [T, (newState: T) => void];
}

React 还利用 namespace 合并的特性,在全局的命名空间中注入了一些类型:

代码语言:javascript复制
declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> {}
  }
}

# 仅类型导入

在 TypeScript 中,导入一个类型时,并不需要额外的操作,和导入一个实际值是完全一样的:

代码语言:javascript复制
// foo.ts
export const Foo = () => {};

export type FooType = any;

// index.ts
import { Foo, FooType } from './foo';

虽然类型导入和值导入存在于同一条导入语句中,在编译后的 JS 代码中还是只有值导入存在,同时在编译的过程中,值与类型所在的内存空间也是分开的。

还可以使用 import type 语法:

代码语言:javascript复制
import { Foo } from './foo.ts';
import type { FooType } from './foo.ts';

// 也可以简写
import { Foo, type FooType } from './foo.ts';

一般建议的导入顺序:

  • React
  • 第三方 UI 库,项目内封装的组件
  • 三方工具库,项目内封装的工具方法
  • 类型导入
    • 三方类型导入
    • 项目内类型导入
  • 样式文件

0 人点赞