在流程图或可视化编排工具中,节点之间的连线常采用正交路由。正交路由由若干水平和垂直线段组成,在转弯处形成九十度角。默认直角虽然清晰,但在某些设计语言中会显得生硬,所以需要给拐角增加圆角处理。圆角半径并不是一个简单的数值,它可能表示关闭圆角、统一圆角、百分比圆角或每个拐角单独设置。TypeScript 的类型定义需要把这些可能都表达清楚,同时保持调用方能获得准确的类型提示。本文会从取值形态、类型建模、归一化处理以及路由计算集成几个层面展开。

拐角半径的取值形态与类型挑战
正交路由的拐角半径首先可能是一个固定的逻辑像素值,比如 12 表示所有拐角都按 12 像素绘制圆角。其次,它也可能采用百分比,例如 20% 表示半径取相邻线段长度的一定比例,这样在不同缩放级别下视觉比例保持一致。更复杂的情况是四个拐角分别设置,例如上方两个拐角使用较小半径,下方两个拐角使用较大半径,以模拟某种物理弯曲效果。此外,还需要一个明确的关闭状态,用来表示完全不绘制圆角。
如果直接在 TypeScript 中把类型声明为 number | string,表面上可以接收数字和字符串,但实际上无法表示按角独立配置的对象,也无法约束百分比字符串的格式。更关键的是,调用者拿到这个类型后,只能靠阅读文档或猜测来了解哪些值合法,编辑器也无法提供精确补全。下面这段代码展示了这种宽松类型存在的问题:
type CornerRadiusValue = number | string;
const radius1: CornerRadiusValue = 12;
const radius2: CornerRadiusValue = "20%";
const radius3: CornerRadiusValue = { topLeft: 8, topRight: 8, bottomRight: 12, bottomLeft: 4 }; // 类型错误
可以看到,对象形式的按角配置无法通过编译。更麻烦的是,即使传入一个不合法字符串,例如 "large",也会被当作有效值,直到运行时才可能暴露问题。因此,需要设计一种能够承载多种模式、同时具备编译期检查能力的类型。
用可辨识联合定义拐角半径类型
可辨识联合是 TypeScript 中处理多形态数据的常用手段。我们可以为拐角半径定义一个 kind 字段,用它作为判别属性,将不同模式的参数放到各自成员里。这样每个成员只包含该模式需要的字段,结构清晰,也便于后续扩展。
type CornerRadius =
| { kind: "none" }
| { kind: "fixed"; value: number }
| { kind: "percent"; value: number }
| {
kind: "perCorner";
topLeft: number;
topRight: number;
bottomRight: number;
bottomLeft: number;
};
在这个定义中,none 表示关闭圆角,fixed 表示统一固定像素值,percent 表示相对基准长度的百分比,perCorner 表示四个拐角独立设置。每个成员都使用字符串字面量类型作为 kind,因此联合类型在比较时能够精确区分。
这种设计最大的好处是类型安全。当你在函数中对 radius.kind 进行 switch 判断时,TypeScript 能自动收窄剩余类型。比如在 case "fixed" 分支里,编译器知道 radius.value 一定存在,并且类型为 number。如果后续需要新增一种模式,只需在联合类型中追加成员,所有遗漏处理的函数都会在编译期提示错误。
还可以为常用配置定义常量或工厂函数,降低调用方的书写成本。例如定义一个全零半径的只读常量,并在选项接口中直接复用。不过核心仍然是把“无圆角”和“数值圆角”放在同一套类型体系里,而不是用 undefined、null 和数字混用。
归一化函数与类型守卫
路由算法通常不希望在计算每个拐角时反复判断半径模式,它更希望得到一个统一的四角半径结构。因此可以在类型层和算法层之间增加一层归一化函数,把各种 CornerRadius 输入转换为固定结构。
interface NormalizedCornerRadius {
topLeft: number;
topRight: number;
bottomRight: number;
bottomLeft: number;
}
function normalizeCornerRadius(
radius: CornerRadius,
baseSize?: number
): NormalizedCornerRadius {
switch (radius.kind) {
case "none":
return { topLeft: 0, topRight: 0, bottomRight: 0, bottomLeft: 0 };
case "fixed":
return {
topLeft: radius.value,
topRight: radius.value,
bottomRight: radius.value,
bottomLeft: radius.value,
};
case "percent":
if (baseSize === undefined) {
throw new Error("percent radius requires baseSize");
}
return {
topLeft: baseSize * radius.value / 100,
topRight: baseSize * radius.value / 100,
bottomRight: baseSize * radius.value / 100,
bottomLeft: baseSize * radius.value / 100,
};
case "perCorner":
return { ...radius };
}
}
在 percent 分支中,函数要求额外传入一个基准长度。这个基准长度通常可以取当前线段长度、节点尺寸或画布缩放后的单位长度。抛出错误的方式可以换成返回默认值,但显式抛出能让配置问题更早暴露。对于 perCorner 分支,由于对象中包含了与目标结构相同的字段,可以直接展开返回。
除了归一化,还可以使用类型守卫来让调用方在不归一化的情况下安全读取某个模式的数据。下面这个守卫判断半径是否为按角独立模式:
function isPerCornerRadius(
radius: CornerRadius
): radius is Extract<CornerRadius, { kind: 'perCorner' }> {
return radius.kind === 'perCorner';
}
当 isPerCornerRadius 返回 true 后,TypeScript 会将 radius 收窄到包含 topLeft、topRight 等字段的具体成员。这样在设置面板或校验逻辑中,就能根据当前模式显示不同的输入控件,同时保持类型安全。
接入正交路由配置与计算
定义好拐角半径类型后,需要将它放到节点连线或正交路由的配置接口中。一个常见的做法是让 cornerRadius 成为可选字段,并在未提供时默认关闭圆角。
interface Point {
x: number;
y: number;
}
interface OrthogonalRouteConfig {
source: Point;
target: Point;
cornerRadius?: CornerRadius;
baseSize?: number;
}
在实际构建路由线段时,可以先调用归一化函数得到每个拐角的圆角半径。对于正交连线来说,每个拐角都与一个水平线段和一个垂直线段相邻,当半径大于零时,需要在该拐角处用一段圆弧替代原来的直角。下面是一个简化的拐角圆弧计算函数,它接收前一个点、当前拐点和下一个点,以及归一化后的半径,返回圆弧参数或空值。
interface CornerArc {
center: Point;
radius: number;
startAngle: number;
endAngle: number;
}
function getCornerArc(
prev: Point,
corner: Point,
next: Point,
radius: number
): CornerArc | null {
if (radius <= 0) {
return null;
}
const horizontalFirst = prev.y === corner.y;
let center: Point;
if (horizontalFirst) {
const direction = next.y > corner.y ? radius : -radius;
center = { x: corner.x, y: corner.y + direction };
} else {
const direction = next.x > corner.x ? radius : -radius;
center = { x: corner.x + direction, y: corner.y };
}
return {
center,
radius,
startAngle: horizontalFirst ? Math.PI : Math.PI / 2,
endAngle: horizontalFirst ? Math.PI / 2 : 0,
};
}
这里的 getCornerArc 仅用于说明如何在路由模块中使用半径数据。实际项目中还需要考虑缩放比例、坐标变换以及相邻线段的长度是否足以容纳圆角。如果线段长度过短,半径应当被钳制到不超过较短线段的一半,否则圆弧会越过节点边界。
为了让类型更具表达力,还可以引入可辨识预设或品牌类型。例如用 preset: "sharp" | "rounded" 快速生成对应 CornerRadius,或者用接口继承方式扩展出 auto 模式,让路由算法根据节点尺寸自动设置半径。无论采用哪种扩展,关键是保持判别字段的唯一性,并让所有转换函数都强制处理新增成员。
TypeScript 的类型系统无法在编译期验证百分比数值是否在 0 到 100 之间,因此如果业务要求严格,可以在归一化函数的 percent 分支中加入运行时校验,或者使用模板字面量类型构造更精确的字符串形式。不过对于大多数流程图编辑器来说,可辨识联合配合明确的运行时错误已经能在开发体验和项目复杂度之间取得较好平衡。
TypeScript正交路由拐角半径修改时间:2026-09-01 00:45:02