TIPS: 本篇指南面向未接触过 TypeScript 或刚刚接触 TypeScript 的初学者,欢迎经验丰富的 TypeScript 高手进行指正。
随着微软的大力推广,越来越多的开发者开始使用 TypeScript,TypeScript 正在变得越来越流行,前端开发者们开始体验到静态类型的强大,并开始告别『动态一时爽,重构火葬场』的尴尬境地。
但同时,目前仍有大量第三方类库或公司内部历史项目仍是用纯 js 编写的,将这些代码全部重构成 TypeScript 显然是不现实的,贴心的 TypeScript 团队也为我们提供了另一种更轻便的方式让开发者快速地将第三方包集成到自己的 TypeScript 项目中,这就是今天要介绍的.d.ts
类型文件。
.d.ts
中的d
可以理解为 definition 或 declaration 即『定义或声明』,顾名思义这是对 js 文件的类型声明文件,因此我们需要关注的是对包中导出内容相关的类型,而不必关心其具体实现,简单来说,我们只需要关心包的公开 API 即可。
首先我们需要明确包使用的导出规范,global/umd/commonjs/module 等。
对于 global 导出的包我们使用:
declare namesapce MyLib {
class A {}
// 我们可以直接在代码中使用
// const a = new MyLib.A()
}
对于 umd/commonjs 导出的包我们使用:
declare module 'my-lib' {
namespace MyLib {
class A {}
class B {}
// 使用时
// 我们可以使用
// import * as MyLib from 'my-lib'
// const a = new MyLib.A();
// 如果开启了 ES Module 融合模式 (esModuleInterop=true)
// 我们可以使用
// import { A } from 'my-lib'
// const a = new A()
}
export = MyLib
}
对于 ES Module 导出的包我们使用:
declare module 'my-lib' {
class MyLib {}
export default MyLib
// or other exorts
export class A {}
// 我们可以使用
// import MyLib, {A} from 'my-lib'
// const lib = new MyLib()
// const a = new A()
}
简单了解了不同模块导出格式的书写我们开始正式根据 API 来编写内容。
我们以一个 API 非常简单的包invert-color为例,它只对外提供 3 个 API:
invert(color[, bw])
invert.asRGB(color[, bw])
invert.asRgbArray(color[, bw])
具体到invert(color[, bw])
中,color
类型为String|Array|Object
,bw
可选,类型为Boolean|Object
,返回值均为String
,即 :
// 错误示范
export const invert: (color: String | Array | Object, bw?: Boolean | Object) => String
首先 TypeScript 类型声明我们不应该使用Number/String/Boolean/Object
等包装类(这些类在 js 中很少直接使用并以 API 示人),而应该直接使用基本类型声明number/string/boolean/object
,另外我们看到示范中的Array/Object
对我们并没有特别好的提示作用,我们调用时依然无法知道该传什么类型的数组,对象的格式应该是什么样的,因此我们需要再查看一下 API 文档并重新定义一下类型:
// 字面量数组类型,明确提示应使用长度为 3 且类型均为数字的数组,对于明确长度的数组定义我们不应该使用 number[] 这样的不定长度数组进行声明
export type RgbArray = [number, number, number]
// 对于对象类型,我们使用 interface 替代 type 声明,方便可能的扩展和继承
export interface RGB {
r: number
g: number
b: number
}
export interface BlackWhite {
black: string
white: string
threshold?: number
}
export const invert = (color: string | RgbArray | RGB, bw?: boolean | BlackWhite) => string
可以看到我们的类型声明文件一下长了许多,但这将为我们使用invert
方法提供更安全、更优雅的体验。但我们只定义了invert
方法,接下来考虑一下invert.asRGB
和invert.asRgbArray
,这两个方法均挂载在invert
函数上,一般的我们会用namespace
来添加属性方法,但目前 TypeScript 开始推荐使用 ES Module 融合模式而不是namespace
来解决,因此这里我们来使用另一种方式扩展:
export type Color = string | RgbArray | RGB
export interface InvertColor {
(color: Color, bw?: boolean | BlackWhite): string // interface 可以直接定义函数体
asRGB(color: Color, bw?: boolean | BlackWhite): RGB
asRgbArray(color: Color, bw?: boolean | BlackWhite): RgbArray
}
export const invert: InvertColor;
至此,一个完整的.d.ts
类型声明就完成了!
接下来,我们开始介绍一下其他技巧:
// 函数重载,我们应只对返回值不同的函数进行声明重载,否则可以将参数进行合并
function fn(): number
function fn(input: string): string
// 泛型,可以使用时动态设置返回值类型
export function request<T>(): Promise<T>
// 如 const result = request<{a: string}>(); result.a
// 动态键值对类型,配合泛型可以定义一个通用接口
export interface KeyValue<T = any> {
locked: boolean // 也可以有部分固定的属性
[key: string]: T
}
// TypeScript 提供一个通用的全局 Record 类型
type R = Record<string, string>
// keyof 获取对象所有 key 值字面量
interface Value {
key1: string
key2: boolean
}
type Keys = keyof Value // 即 type Keys = 'key1' | 'key2'
// in 遍历字面量类型
type Value2 = {
[key in Keys]: Value[key]
}
// 即 type Values = { key1: string; key2: boolean }
// typeof 获取字面量的类型
class A {}
// 这里表示参数是 class A 这个类,如果没有 `typeof` 则参数是 class A 的实例
export const callClassA(classA: typeof A)
// Exclude 排除指定类型
type Key1 = Exclude<Keys, 'key2'> // 即 type Key1 = 'key1'
// 以下扩展全局属性的方式应尽量避免使用
// 扩展新增全局变量
declare global {
const myGlobalVar: string // 将能在全局直接使用 myGlobalVar
}
// 扩展新增 Node global 属性
declare namespace NodeJS {
interface Global {
myGlobalVar: string // 将能在全局使用 global.myGlobalVar
}
}
// 扩展新增 window 属性
declare interface Window {
myGlobalVar: string // 将能在全局使用 window.myGlobalVar
}
// 针对文件后缀声明类型,如使用 CSS Modules
declare module '*.less' {
const styles: Record<string, string>;
export = styles;
}
虽然有这么多类型声明的技巧,但其实最后的难点在于如何把不同的部分组合使用起来,我们以一个相对复杂的实例hoist-non-react-statics作为收尾:
import * as React from 'react';
interface REACT_STATICS {
childContextTypes: true;
contextTypes: true;
defaultProps: true;
displayName: true;
getDefaultProps: true;
getDerivedStateFromProps: true;
mixins: true;
propTypes: true;
type: true;
}
interface KNOWN_STATICS {
name: true;
length: true;
prototype: true;
caller: true;
callee: true;
arguments: true;
arity: true;
}
declare function hoistNonReactStatics<
T extends React.ComponentType<any>,
S extends React.ComponentType<any>,
C extends {
[key: string]: true
} = {}
>(
TargetComponent: T,
SourceComponent: S,
customStatic?: C,
): T &
{
[key in Exclude<
keyof S,
// only extends static properties, exclude instance properties and known react statics
keyof REACT_STATICS | keyof KNOWN_STATICS | keyof C
>]: S[key]
};
export = hoistNonReactStatics;
大家可以自行理解一下上面复制但排除指定静态属性的定义部分,如果能够理解那么相信本篇指南对你有效,如果没有欢迎评论探讨疑问。
大家也可以到DefinitelyTyped查看更多优质类型定义文件,并贡献自己的力量。
希望大家工作之余有额外的精力时可以帮助完善公司内部基础库的类型定义声明,让一线开发者们即使没有文档也能写出稳健的代码,当然更美好的目标是:现在就开始使用 TypeScript 来编写源代码。