Typescript中工程层面的类型能力
TIP
今天来扩展工程化层面的类型知识
类型检查指令
行检查
- 关闭下一行的Ts代码检查
// @ts-ignore
const name: string = 100;
- 关闭下一行的Ts代码检查,前提是真的有错误
// @ts-expect-error
const name: string = 100;
const age: number = 599; // 报错
NOTE
无条件使用ts-expect-error
指令, 确定它真的是有错误而不得不去屏蔽它
文件检查
- 关闭文件检查
// @ts-nocheck
const name: string = 100;
const age: number = 'Harexs';
- 开启文件检查
@ts-chekc 主要适用于JS文件的主动检查,配合JSDoc使用
类型声明
通过declare
关键字可以定义类型声明,它们不会被实际打包到生产代码中,而是在编译层面上提供类型检查和类型信息
需注意它们不能被赋值
declare var f1: () => void;
declare interface Foo {
prop: string;
}
declare function foo(input: Foo): Foo;
declare class Foo {}
// × 不允许在环境上下文中使用初始值
declare let result = foo();
// √ Foo
declare let result: ReturnType<typeof foo>;
类型覆盖和扩展
通常.d.ts
文件都会被TS作为类型声明文件自动加载到环境中实现代码补全,但对于无类型的npm
包,我们可以通过declare module
提供类型
import foo from 'pkg'
const res = foo.handler() //无类型提示
// x.d.ts
declare module 'pkg'{
export const handler: ()=> boolean;
} // 此时上面的res就有布尔的返回类型了
declare module 'pkg2' {
const handler: () => boolean;
export default handler;
}
import bar from 'pkg2';
bar();
非代码文件
对于非代码文件,其本质和包文件一样是一个导入语句,也可以通过声明来决定其内容和类型
// x.d.ts
declare module '*.md' {
const raw: string;
export default raw;
}
//index.ts
import raw from './1.md'
const content = raw.replace('123','321')
DefinitelyTyped
在npm上通常可以看到有@types/
开头的包,这类都属于由TS官方维护的无类型定义的JS库的类型支持包,typescript会自动加载所有@types/
下的包,比如@types/node
和@type/react
中:
// @types/node
declare module 'fs' {
export function readFileSync(/** 省略 */): Buffer;
}
// @types/react
declare namespace React {
function useState<S>(): [S, Dispatch<SetStateAction<S>>];
}
前者提供了单独的模块声明, 后者则是使用命名空间做了统一声明
扩展类型
对于没有类型声明的包或者需要扩展的变量,如window
下需要挂载一个全局变量, 则通过利用接口合并的特性进行扩展
interface Window {
userTracker: (...args: any[]) => Promise<void>;
}
window.userTracker("click!")
当然也可以扩展已经有类型的包的定义,模块的合并特性有个别区别,命名空间则与接口一致
declare module 'fs' {
export function bump(): void; // 如果是模块增强(合并),至少需要包含一个import或者export,否则报错
}
import { bump } from 'fs';
namespace MyNamespace {
export const a = 1;
}
namespace MyNamespace {
export const b = 2;
}
三斜线指令
三斜线指令就是声明文件的导入语句,让当前文件依赖其他类型声明文件,包含TS内置类型
该声明应放置于文件顶部
/// <reference path="./other.d.ts" />
/// <reference types="node" />
/// <reference lib="dom" />
- path类型会根据当前位置一直向上查找,直到找到第一个类型文件
- types类型则是根据
@types/
类型包进行查找 - lib类型则是Ts的内置类型声明
命名空间
除了在类型文件中使用, 在类型声明文件中也可以使用命名空间,它的作用和模块类似,是模块还没完全实现前的模块化方案
// react.d.ts
export = React; // 表示整个模块导出的值就是 React 这个对象
export as namespace React; // 即使没有 import React from 'react',也可以在全局直接使用 React.useState 这样的类型
declare namespace React { // 声明了一个命名空间 React,里面包含了一堆类型和函数签名
// 倘若对命名空间使用了导出,那么内部的声明则不再需要再次declare
function useState<S>(initialState): [];
}
// ESM风格则必须要声明对应的类型变量来明确导出
declare namespace React {
function useState<S>(initialState: S): [S, (value: S) => void];
}
declare const React: {
useState: typeof React.useState;
};
export default React;
export { React };
还可以利用命名空间合并特性,在全局命名空间注入其他类型,来提供类型支持
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
}
}
类型导入
Ts提供了单独的类型导入语法, 以更好区分实际的模块导入,和类型导入
import { Foo } from "./foo";
import type { FooType } from "./foo";
// 或者
import { Foo, type FooType } from "./foo";
根据类型编排导入顺序
import { useEffect } from 'react';
import { Button, Dialog } from 'ui';
import { ChildComp } from './child';
import { store } from '@/store'
import { useCookie } from '@/hooks/useCookie';
import { SOME_CONSTANTS } from '@/utils/constants';
import type { FC } from 'react';
import type { Foo } from '@/typings/foo';
import type { Shared } from '@/typings/shared';
import styles from './index.module.scss';
TsConfig配置
特殊属性
experimentalDecorators 与 emitDecoratorMetadata
主要用于装饰器相关,如NestJs
和Ioc
相关的框架或者工具中基本都会开启这个属性, 支持装饰器和元数据特性
target 与 lib
target
决定构建代码的语法, 其中esnext
代表最新的支持版本, 它会影响lib
加载的对应类型文件, 如果你发现replaceAll
提示不存在,通常就是指定的版本过低了
构建解析
files、include 与 exclude
files代表使用到的所有文件, 通常只在小项目中会用到, include和exclude则是通过正则匹配会用到的文件和排除文件
exclude中剔除的文件必须在include中存在
{
"compilerOptions": {},
"files": [
"src/index.ts",
"src/handler.ts"
],
"include": ["src/**/*", "generated/*.ts", "internal/*"],
"exclude": ["src/file-excluded", "/**/*.test.ts", "/**/*.e2e.ts"]
}
baseUrl
决定文件解析的根目录,通常是相对路径,根据tsocnfig.json
的所在目录来确定,通常是"baseUrl":"./"
types 与 typeRoots
决定类型文件加载的逻辑,通常会默认加载/@types/
下的所有声明文件,通过types可以指定类型
{
"compilerOptions": {
"types": ["node", "jest", "react"]
}
}
而typeRoots则决定了 所有声明文件的加载来源
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./node_modules/@team-types", "./typings"],
"types": ["react"],
"skipLibCheck": true
}
}
上面这个代码中,Ts还会从@team-types ./typings
下去加载所有类型声明文件
paths
类似打包构建的alias
即别名,让类型导入简化
通常这个属性要配合baseUrl一起使用,更清晰的知道解析路径走向
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/utils/*": ["src/utils/*", "src/other/utils/*"]
}
}
}
resolveJsonModule
支持JSON文件的导入和类型支持
构建产物
noEmit 与 noEmitOnError
控制是否将产物写入, 如果没开启的情况下则是仅做类型检查、语法检查的操作,比如很多时候会见到的tsc --noEmit
module
控制产物的模块标准,通常不需要特别调整,默认使用ESNext
保证最新的特性,如果是纯Node环境
则可以用nodenext
declaration
是否产生声明文件,即.d.ts
之类的文件,需要注意默认情况下不产生的
sourceMap
源文件映射, 根据使用场景决定是否开启
语法检查
noImplicitAny
没有为变量或参数指定类型,同时 TypeScript 也无法自动推导其类型时,这里变量的类型就会被推导为 any。 如果希望没有这种隐式类型导致的任意调用则开启这个配置,显式声明不影响
useUnknownInCatchVariables
改变try/catch
语法中的catch类型为unknown
,而不是默认的any
noUnusedLocals 与 noUnusedParameters
是否允许存在声明但没使用的变量和函数参数
alwaysStrict
对所有文件开启严格模式并在产物写入严格声明
strict
开启内置的严格规则,包含:
alwaysStrict、useUnknownInCatchVariables
noFallthroughCasesInSwitch、noImplicitAny、noImplicitThis
strictNullChecks、strictBindCallApply、strictFunctionTypes、strictPropertyInitialization
strictNullChecks
这是一个必须开启的规则,它通常体现在联合类型中隐式的undefiend、null
类型
const matcher: string = "budu";
const list = ['linbudu', '599'];
// 为 string 类型
const target = list.find((u) => u.includes(matcher));
// target为 string | undefined, 如果不开启规则就可能导致 cannot read property 'xxx' of undefined
console.log(target.replace('budu', 'wuhu'));
工程模块
Project References
通常对于多个模块或者项目之间的引用,不同的模块项目之间可能使用的构建规则不一致,那么这个时候就需要用到引用属性了 假设有一个项目结构是:
PROJECT
├── app
│ ├── index.ts
│ ├── tsconfig.json
├── core
│ ├── index.ts
│ ├── tsconfig.json
├── ui
│ ├── index.ts
│ ├── tsconfig.json
├── utils
│ ├── index.ts
│ ├── tsconfig.json
├── tsconfig.base.json
关系是
app -> core, ui, utils
core -> utils
app
模块
"extends": "../tsconfig.base.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"baseUrl": ".",
"outDir": "../dist/app"
},
"include": ["./**/*.ts"],
"references": [
{
"path": "../utils"
},
{
"path": "../core"
},
{
"path": "../ui"
}
]
}
core
模块
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"outDir": "../dist/core"
},
"include": ["./**/*.ts"],
"references": [
{
"path": "../utils"
}
]
}
其他模块
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"target": "ES5",
"module": "commonjs",
"baseUrl": ".",
"outDir": "../dist/utils"
},
"include": ["./**/*.ts"]
}
接着在主程序中去引用其他模块的功能
import { util } from '../utils';
import { core } from '../core';
import { ui } from '../ui';
export const app = () => {
ui();
core();
util();
console.log('app!');
};
composite
使用引用属性时,必须在被引用的子项目开启此属性,以及declaration属性、includes或者files 得到类型输出和文件关系
extends
复用模块配置文件,类似ESlint中的extends