在信号处理、图形变换和电磁场计算等场景里,复数常常需要用极坐标来描述,因为旋转和缩放操作在极坐标下只是辐角相加、模长相乘。Complex.js 的 Complex 对象已经提供了 abs() 和 arg() 方法,能直接拿到模长和辐角,但这两个返回值都是没有单位的 number,调用方很难判断传入的角度是弧度还是角度制,也无法约束模长不能为负。如果用 TypeScript 对极坐标表示做一层类型封装,就能在编译阶段减少这类错误。

一、极坐标类型的基础定义
极坐标表示由模长和辐角两个量组成,但在 TypeScript 里如果直接写成 { r: number; phi: number },并不能表达这两个字段的语义差异。r 代表非负实数,phi 代表弧度,数值范围通常约定为 [0, 2π) 或 (-π, π]。为了在类型层面区分普通数字和角度、模长,可以使用品牌类型(branded type)。品牌类型通过给 number 类型附加一个唯一的 symbol,使得普通 number 不能直接赋值给 Radian 或 Modulus,必须经过转换函数。
下面是一个基础定义。Radian 和 Modulus 都是 number 品牌的别名,PolarForm 接口要求 r 和 phi 为只读字段,防止在运算过程中意外修改输入对象。
declare const radianBrand: unique symbol;
declare const modulusBrand: unique symbol;
type Radian = number & { readonly [radianBrand]: never };
type Modulus = number & { readonly [modulusBrand]: never };
interface PolarForm {
readonly r: Modulus;
readonly phi: Radian;
}
function toRadian(value: number): Radian {
return value as Radian;
}
function toModulus(value: number): Modulus {
if (value < 0) {
throw new Error('Modulus must be non-negative');
}
return value as Modulus;
}
这段代码使用 unique symbol 作为品牌标记,避免不同品牌之间发生结构化兼容。toRadian 函数只做简单的类型断言,因为角度本身不涉及范围约束;toModulus 则增加运行时校验,模长为负数时直接抛错。实际封装中,从 Complex.js 获取模长时可以使用 toModulus 保护入口。
品牌类型虽然会增加一些转换代码,但能在大型计算模块中显著降低单位混淆的概率。如果团队不希望引入品牌类型,至少应当定义 PolarForm 接口并配合命名规范,例如字段名使用 r 和 phi,避免使用 magnitude 和 angle 这种容易产生歧义的命名。
二、与 Complex.js 实例的转换和类型守卫
Complex.js 的 Complex 类同时支持直角坐标和极坐标构造。从代数形式得到极坐标类型,通常调用实例的 abs() 和 arg() 方法。由于这两个方法返回普通 number,将它们包装成 PolarForm 时正好可以借助前面定义的 toModulus 和 toRadian。
import Complex from 'complex.js';
function toPolarForm(value: Complex): PolarForm {
return {
r: toModulus(value.abs()),
phi: toRadian(value.arg())
};
}
function toComplex(form: PolarForm): Complex {
return Complex.fromPolar(form.r, form.phi);
}
上面的 toComplex 函数假设 Complex.js 提供了 fromPolar 静态方法。如果你的版本还没有这个方法,可以用 new Complex 加一点三角运算代替,但既然目标是封装极坐标表示,优先使用官方极坐标构造入口可以让转换更直观。
类型守卫在处理未知输入时很重要。例如某个 API 返回的可能是 Complex,也可能是普通对象,不能直接调用 abs。下面的 isComplexLike 函数检查对象上是否存在 abs 和 arg 方法,如果通过,TypeScript 会将其收窄为 Complex 类型。
function isComplexLike(value: unknown): value is Complex {
return typeof value === 'object'
&& value !== null
&& 'abs' in value
&& 'arg' in value;
}
function safeToPolarForm(value: unknown): PolarForm | null {
if (!isComplexLike(value)) {
return null;
}
return toPolarForm(value);
}
safeToPolarForm 在输入不合法时返回 null,而不是抛出异常。这种宽松入口适合解析外部数据,调用方可以通过空值判断决定后续流程。对于内部计算链路,建议使用严格版本的 toPolarForm,尽早暴露错误。类型守卫和转换函数分开,也让单元测试更容易覆盖边界情况。
三、极坐标运算封装与精度控制
极坐标的乘法、除法和幂运算公式简单:乘法模长相乘、辐角相加;除法模长相除、辐角相减;幂运算模长取幂、辐角与指数相乘。把这三类运算封装成只接受 PolarForm 的函数,可以避免调用方自己写三角公式。
function normalizeAngle(phi: number): number {
const twoPi = 2 * Math.PI;
return ((phi % twoPi) + twoPi) % twoPi;
}
function multiplyPolar(a: PolarForm, b: PolarForm): PolarForm {
return {
r: toModulus(a.r * b.r),
phi: toRadian(normalizeAngle(a.phi + b.phi))
};
}
function dividePolar(a: PolarForm, b: PolarForm): PolarForm {
if (b.r === 0) {
throw new Error('Cannot divide by zero modulus');
}
return {
r: toModulus(a.r / b.r),
phi: toRadian(normalizeAngle(a.phi - b.phi))
};
}
function powPolar(base: PolarForm, exponent: number): PolarForm {
return {
r: toModulus(Math.pow(base.r, exponent)),
phi: toRadian(normalizeAngle(base.phi * exponent))
};
}
normalizeAngle 把任意数值映射到 [0, 2π) 区间。采用先取模再加周期再取模的方式,是为了兼容负数输入。浮点运算中 2π 本身无法精确表示,多次加减后角度可能漂移,因此 normalizeAngle 的输出只能保证在约定区间附近,不能保证完全精确。
比较两个极坐标表示是否相等时,直接使用 === 对浮点数几乎不可靠。更合理的做法是引入容差判断,并在比较前对角度做归一化处理。
function polarEquals(a: PolarForm, b: PolarForm, epsilon = 1e-10): boolean {
const phiDiff = Math.abs(normalizeAngle(a.phi - b.phi));
const rDiff = Math.abs(a.r - b.r);
return rDiff < epsilon && phiDiff < epsilon;
}
这个函数默认容差为 1e-10,适合大多数浮点场景。对于高精度计算,可以将 epsilon 调小,但要注意 Complex.js 内部使用双精度浮点,无法真正做到任意精度。角度接近 2π 和 0 时,归一化后的差值可能跨越边界,polarEquals 也能正确处理,因为 normalizeAngle 已经将差值折叠到 [0, 2π)。
四、模块组织与工程化建议
将极坐标类型定义和运算函数放在独立的 polar.ts 文件中,业务代码通过具名导入使用。这样可以避免在多个文件中重复声明接口,也方便后续替换 Complex.js 的底层实现。示例导出如下。
export type { PolarForm, Radian, Modulus };
export {
toRadian,
toModulus,
toPolarForm,
toComplex,
safeToPolarForm,
multiplyPolar,
dividePolar,
powPolar,
polarEquals,
normalizeAngle
};
在实际项目中,建议开启 TypeScript 的 strict 模式和 noUncheckedIndexedAccess 等严格选项。strict 可以确保 noImplicitAny 捕获漏写类型的情况;noUncheckedIndexedAccess 避免从数组或索引签名中取出 undefined。对于品牌类型,断言函数要集中管理,不要在业务代码里到处使用 as Radian,否则品牌保护会失去意义。
下面是一个完整的使用示例,展示从 Complex.js 对象到极坐标封装再回到 Complex 对象的流程。
import Complex from 'complex.js';
import { toPolarForm, multiplyPolar, toComplex } from './polar';
const z1 = new Complex(3, 4);
const z2 = Complex.fromPolar(2, Math.PI / 6);
const p1 = toPolarForm(z1);
const p2 = toPolarForm(z2);
const product = multiplyPolar(p1, p2);
const result = toComplex(product);
console.log(result.toString()); // 输出的代数形式与直接 z1.mul(z2) 一致
这个流程中,p1 和 p2 带有明确的品牌类型,multiplyPolar 只接受 PolarForm,传错参数会在编译期报错。最终通过 toComplex 转换回 Complex 后,仍然可以调用 Complex.js 的完整运算接口。相比直接使用 { abs, arg } 裸对象,这套封装在可读性和安全性上都有明显提升。
如果团队需要把极坐标表示暴露给外部 API,建议为 JSON 序列化定义纯数据接口,并在边界处进行品牌类型与普通 number 之间的转换。品牌类型在运行时并不存在,所以 JSON.stringify 不会包含任何额外标记,但这不影响类型安全,因为安全保证只发生在编译阶段。
TypeScriptComplex.js极坐标表示修改时间:2026-10-01 14:02:50