Skip to content

声明文件进阶

掌握基础声明文件后,本��介绍更复杂的声明场景和高级技巧。

三斜线指令#

三斜线指令是声明文件之间的引用方式:

// TypeScript 5.x
// 引用其他声明文件
/// <reference path="./other.d.ts" />
// 引用 @types 包
/// <reference types="node" />
// 引用 ES 内置库
/// <reference lib="es2015" />
/// <reference lib="dom" />

reference path#

引用本地声明文件���

// === types/base.d.ts ===
interface BaseEntity {
id: string
createdAt: Date
updatedAt: Date
}
// === types/user.d.ts ===
/// <reference path="./base.d.ts" />
interface User extends BaseEntity {
name: string
email: string
}
// === types/index.d.ts ===
/// <reference path="./base.d.ts" />
/// <reference path="./user.d.ts" />

reference types#

引用 npm 包中的类型:

// === my-library.d.ts ===
/// <reference types="node" />
import { EventEmitter } from 'events'
export class MyEmitter extends EventEmitter {
emit(event: 'data', data: Buffer): boolean
emit(event: 'end'): boolean
emit(event: string, ...args: unknown[]): boolean
}

reference lib#

引用内置库:

// 只引用需要的库
/// <reference lib="es2015.promise" />
/// <reference lib="es2015.iterable" />
/// <reference lib="dom" />
// 而不是在 tsconfig.json 中配置

复杂模块声明#

UMD 模块#

同时支持多种模块系统:

my-library.d.ts
export as namespace MyLibrary
export function init(options: Options): void
export function destroy(): void
export interface Options {
debug?: boolean
}
export default class MyLibrary {
constructor(options?: Options)
start(): void
stop(): void
}
// 使用方式
// ES 模块
import MyLibrary from 'my-library'
// CommonJS
const MyLibrary = require('my-library')
// 全局变量(浏览器)
MyLibrary.init({ debug: true })

导出函数和命名空间#

lodash.d.ts
export = _
declare function _(value: string): _.LoDashWrapper<string>
declare function _(value: number): _.LoDashWrapper<number>
declare function _<T>(value: T[]): _.LoDashArrayWrapper<T>
declare function _<T extends object>(value: T): _.LoDashObjectWrapper<T>
declare namespace _ {
interface LoDashWrapper<T> {
value(): T
}
interface LoDashArrayWrapper<T> {
first(): T | undefined
last(): T | undefined
value(): T[]
}
interface LoDashObjectWrapper<T> {
keys(): string[]
values(): unknown[]
value(): T
}
function map<T, R>(array: T[], fn: (item: T) => R): R[]
function filter<T>(array: T[], fn: (item: T) => boolean): T[]
function reduce<T, R>(array: T[], fn: (acc: R, item: T) => R, initial: R): R
}
// 使用
import _ from 'lodash'
const result = _([1, 2, 3]).first()
const mapped = _.map([1, 2, 3], (n) => n * 2)

可调用对象#

// 既是函数又有属性
interface JQuery {
(selector: string): JQueryElement
(element: HTMLElement): JQueryElement
(callback: () => void): void
ajax(options: AjaxOptions): Promise<unknown>
get(url: string): Promise<unknown>
post(url: string, data: unknown): Promise<unknown>
fn: JQueryPrototype
version: string
}
interface JQueryElement {
html(): string
html(content: string): this
css(property: string): string
css(property: string, value: string): this
addClass(className: string): this
removeClass(className: string): this
on(event: string, handler: (e: Event) => void): this
}
interface AjaxOptions {
url: string
method?: string
data?: unknown
}
interface JQueryPrototype {
extend(plugin: Record<string, Function>): void
}
declare const $: JQuery

类的静态和实例#

// 分离静态和实例类型
interface UserConstructor {
new (name: string, email: string): UserInstance
fromJSON(json: string): UserInstance
defaultRole: string
}
interface UserInstance {
name: string
email: string
greet(): string
toJSON(): string
}
declare const User: UserConstructor
// 使用
const user = new User('张三', 'test@test.com')
user.greet()
const user2 = User.fromJSON('{}')
console.log(User.defaultRole)

全局声明#

在模块中声明全局#

globals.d.ts
export {}
declare global {
// 全局变量
const __DEV__: boolean
const __VERSION__: string
// 全局函数
function __log__(message: string): void
// 扩展 Window
interface Window {
dataLayer: unknown[]
gtag(...args: unknown[]): void
}
// 扩展 NodeJS
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test'
DATABASE_URL: string
API_KEY: string
}
}
// 扩展内置类型
interface Array<T> {
unique(): T[]
groupBy<K extends string | number>(fn: (item: T) => K): Record<K, T[]>
}
interface String {
toTitleCase(): string
}
}

环境声明#

env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_TITLE: string
readonly VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
// 使用
console.log(import.meta.env.VITE_API_URL)

声明合并#

接口合并#

// 扩展第三方库的接口
import 'express-serve-static-core'
declare module 'express-serve-static-core' {
interface Request {
user?: {
id: string
role: string
}
startTime?: number
}
interface Response {
success<T>(data: T): void
fail(error: Error): void
}
}
// 使用
import express from 'express'
const app = express()
app.use((req, res, next) => {
req.startTime = Date.now()
next()
})
app.get('/', (req, res) => {
res.success({ message: 'ok' })
})

命名空间合并#

// 扩展枚举
enum Color {
Red,
Green,
Blue,
}
namespace Color {
export function fromString(str: string): Color | undefined {
switch (str.toLowerCase()) {
case 'red':
return Color.Red
case 'green':
return Color.Green
case 'blue':
return Color.Blue
default:
return undefined
}
}
export function toString(color: Color): string {
return Color[color].toLowerCase()
}
}
// 使用
const color = Color.fromString('red')
const str = Color.toString(Color.Blue)

条件类型在声明中的应用#

// 根据参数类型返回不同类型
declare function createElement<T extends keyof HTMLElementTagNameMap>(
tag: T
): HTMLElementTagNameMap[T]
declare function createElement(tag: string): HTMLElement
// 使用
const div = createElement('div') // HTMLDivElement
const span = createElement('span') // HTMLSpanElement
const custom = createElement('my-element') // HTMLElement
// 事件处理器声明
interface EventMap {
click: MouseEvent
keydown: KeyboardEvent
focus: FocusEvent
custom: CustomEvent
}
declare function addEventListener<K extends keyof EventMap>(
type: K,
listener: (event: EventMap[K]) => void
): void
declare function addEventListener(
type: string,
listener: (event: Event) => void
): void
// 使用
addEventListener('click', (e) => {
console.log(e.clientX) // MouseEvent
})
addEventListener('keydown', (e) => {
console.log(e.key) // KeyboardEvent
})

泛型声明#

// 复杂泛型函数
declare function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>
declare function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>
declare function merge<T, U>(target: T, source: U): T & U
// Promise 工具
declare function promiseAll<T extends readonly unknown[]>(
promises: T
): Promise<{ [K in keyof T]: Awaited<T[K]> }>
declare function promiseRace<T>(promises: Promise<T>[]): Promise<T>
// 类型推断
declare function identity<T>(value: T): T
declare function pair<T, U>(first: T, second: U): [T, U]
// 条件泛型
declare function isDefined<T>(value: T | undefined | null): value is T
declare function assertDefined<T>(
value: T | undefined | null
): asserts value is T

实际案例#

React 组件声明#

react-components.d.ts
import * as React from 'react'
declare module 'my-ui-library' {
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'small' | 'medium' | 'large'
disabled?: boolean
loading?: boolean
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
children: React.ReactNode
}
export const Button: React.FC<ButtonProps>
export interface InputProps {
value?: string
defaultValue?: string
placeholder?: string
onChange?: (value: string) => void
onBlur?: () => void
}
export const Input: React.ForwardRefExoticComponent<
InputProps & React.RefAttributes<HTMLInputElement>
>
export interface ModalProps {
open: boolean
onClose: () => void
title?: string
children: React.ReactNode
}
export const Modal: React.FC<ModalProps>
}

数据库客户端声明#

database.d.ts
declare module 'my-database' {
export interface QueryResult<T> {
rows: T[]
rowCount: number
command: string
}
export interface ConnectionConfig {
host: string
port: number
database: string
user: string
password: string
}
export class Client {
constructor(config: ConnectionConfig)
connect(): Promise<void>
disconnect(): Promise<void>
query<T = unknown>(sql: string, params?: unknown[]): Promise<QueryResult<T>>
transaction<T>(fn: (client: Client) => Promise<T>): Promise<T>
}
export class Pool {
constructor(config: ConnectionConfig & { max?: number })
query<T = unknown>(sql: string, params?: unknown[]): Promise<QueryResult<T>>
getClient(): Promise<Client>
end(): Promise<void>
}
}

API 客户端声明#

api-client.d.ts
declare module 'api-client' {
export interface RequestConfig {
baseURL?: string
timeout?: number
headers?: Record<string, string>
}
export interface Response<T> {
data: T
status: number
headers: Record<string, string>
}
export interface ApiClient {
get<T>(url: string, config?: RequestConfig): Promise<Response<T>>
post<T>(
url: string,
data?: unknown,
config?: RequestConfig
): Promise<Response<T>>
put<T>(
url: string,
data?: unknown,
config?: RequestConfig
): Promise<Response<T>>
delete<T>(url: string, config?: RequestConfig): Promise<Response<T>>
interceptors: {
request: InterceptorManager<RequestConfig>
response: InterceptorManager<Response<unknown>>
}
}
export interface InterceptorManager<T> {
use(
onFulfilled?: (value: T) => T | Promise<T>,
onRejected?: (error: unknown) => unknown
): number
eject(id: number): void
}
export function create(config?: RequestConfig): ApiClient
const defaultClient: ApiClient
export default defaultClient
}

常见问题#

🙋 如何处理 this 类型?#

// 链式调用声明
declare class Builder<T> {
set<K extends string, V>(key: K, value: V): Builder<T & { [P in K]: V }>
build(): T
}
// 使用
const obj = new Builder().set('name', '张三').set('age', 25).build()
// { name: string; age: number }

🙋 如何声明回调函数?#

// 回调函数类型
type Callback<T> = (error: Error | null, result?: T) => void
declare function readFile(path: string, callback: Callback<string>): void
declare function readFileAsync(path: string): Promise<string>
// 事件回调
type EventCallback<T> = (event: T) => void
declare function on<K extends keyof EventMap>(
event: K,
callback: EventCallback<EventMap[K]>
): void

🙋 如何测试声明文件?#

// 创建测试文件 test.ts
import MyLib from 'my-lib'
// 测试类型是否正确
const lib = new MyLib() // $ExpectType MyLib
lib.method() // $ExpectType string
// 测试类型错误
// @ts-expect-error
lib.method(123) // 应该报错
// 运行 tsc --noEmit 检查

总结#

特性语法用途
reference path/// <reference path="" />引用本地声明
reference types/// <reference types="" />引用类型包
reference lib/// <reference lib="" />引用内置库
UMDexport as namespace支持全局和模块
全局声明declare global模块中添加全局
声明合并同名声明扩展现有类型

下一篇我们将学习 tsconfig 配置,了解 TypeScript 项目的配置选项。