# 类型检查指令
# ts-ignore 与 ts-expect-error
ts-ignore
直接禁用对下一行代码的类型检查,其本质是 ignore
而不是 disable
:
// @ts-ignore
const a: number = 'string'
如果代码并没有问题,ignore
反而是错误了,因此引入了更严格版本的 ignore
,即 ts-expect-error
,它只有在下一行代码真存在错误时才能被使用,否则会报错:
// @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 文件都不再接受类型检查:
// @ts-nocheck
const a: number = 'string'
const b: string = 1
ts-check
看起来比较多余,因为默认 TS 文件都会被检查。但实际上,这两个指令还可以用在 JS 文件中。TypeScript 并不是只能检查 TS 文件,对于 JS 文件也可以通过类型推导与 JSDoc 的方式进行不完全的类型检查:
// JavaScript 文件
let myAge = 18;
// JSDoc 标注类型
/** @type {string} */
let myName;
class Foo {
prop = 599;
}
声明了初始值的 myAge
与 Foo.prop
都能被推导出其类型,而无初始值的 myName
也可以通过 JSDoc 标注的方式来显式标注类型。
如果希望在 JS 文件中也能享受类型检查,此时 ts-check
指令既可以登场:
// @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
文件,后者是类型声明文件:
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
来提供其类型:
import foo from 'pkg';
const res = foo.handler();
添加类型提示:
代码语言:javascript复制declare module 'pkg' {
const handler: () => boolean;
}
可以在 declare module
中使用默认导出:
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" />
注意,三斜线指令必须放置在文件的顶部才能生效。
使用 path
的 reference
指令,其 path
属性的值为一个相对路径,指向你项目内的其他声明文件。在编译时,TS 会沿着 path
指定的路径不断深入寻找,最深的那个没有其他依赖的声明文件会被最先加载。
使用 types
的 reference
指令,其 types
的值是一个包名,也就是想引入的 @types/
声明,如 /// <reference types="node" />
是在声明当前文件对 @types/node
的依赖。如果代码文件(.ts
)中声明了对某一个包的类型导入,那再编译产生的声明文件(.d.ts
)中就会自动添加对应的 reference
指令。
使用 lib
的 reference
指令,其 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
:
declare namespace Animal {
export interface Dog {}
export interface Cat {}
}
declare let dog: Animal.Dog;
declare let cat: Animal.Cat;
在 @types/
系列的包下,想通过 namespace
进行模块的声明,还需要注意将其导出,然后才会加载到对应的模块下:
export = React;
export as namespace React;
declare namespace React {
// ...
function useState<T>(initialState: T | (() => T)): [T, (newState: T) => void];
}
React 还利用 namespace
合并的特性,在全局的命名空间中注入了一些类型:
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
语法:
import { Foo } from './foo.ts';
import type { FooType } from './foo.ts';
// 也可以简写
import { Foo, type FooType } from './foo.ts';
一般建议的导入顺序:
- React
- 第三方 UI 库,项目内封装的组件
- 三方工具库,项目内封装的工具方法
- 类型导入
- 三方类型导入
- 项目内类型导入
- 样式文件