在流程图画布上,节点连线如果直接使用直线,路径很容易穿过其他节点甚至覆盖文字,尤其在节点密集的架构图或状态机图中几乎无法使用。正交路由通过只允许水平线段和垂直线段来保持连线整齐,但避障才是核心难点:障碍物不是固定的,用户拖拽节点、调整画布大小、批量添加组件时,可用空间都会实时变化。动态避障算法需要根据场景在A星、跳点搜索、可见性图、栅格搜索等策略之间切换。如果直接用any表示配置,编译器无法帮你发现拼写错误或参数不匹配。TypeScript的联合类型、映射类型和接口合并可以构建一个既支持动态选择又保持类型安全的算法选择类型体系。本文以一个不依赖具体图形库的模型为例,说明如何从几何类型抽象逐步走到可扩展的算法注册表。

先建立正交路由的几何与算法接口
正交路由的输出并不是简单的路径点数组,它需要表达每一段是水平还是垂直,以便后续渲染直角拐弯和箭头。我们先把点、矩形障碍物、连线片段、最终路由结果定义为基础接口。障碍物集合用矩形数组表示,画布边界也作为一个大矩形,可以防止路径跑出可视区域。
interface Point {
x: number;
y: number;
}
interface Rect {
x: number;
y: number;
width: number;
height: number;
}
type Direction = 'horizontal' | 'vertical';
interface RouteSegment {
start: Point;
end: Point;
direction: Direction;
}
interface OrthogonalRoute {
segments: RouteSegment[];
totalLength: number;
}
interface AvoidanceContext {
obstacles: Rect[];
bounds: Rect;
}
上面这些接口刻意保持独立,因为算法只依赖上下文,不关心节点内部的业务字段。RoutingAlgorithm中的kind暂时声明为string,是为了第一步验证不同实现可以统一调用;后面会把它收紧为可穷举的字面量联合类型。注意route方法接收起点、终点和AvoidanceContext,返回OrthogonalRoute,这样调用方不需要知道算法内部是栅格化还是基于多边形计算。
interface RoutingAlgorithm {
readonly kind: string;
route(start: Point, end: Point, ctx: AvoidanceContext): OrthogonalRoute;
}
class AStarRouter implements RoutingAlgorithm {
readonly kind = 'astar';
route(start: Point, end: Point, ctx: AvoidanceContext): OrthogonalRoute {
// 实际A星搜索实现省略
return { segments: [], totalLength: 0 };
}
}
这种抽象的好处是,流程图编辑器中的连线服务可以依赖RoutingAlgorithm接口编程,而不是依赖具体类。比如连线拖拽到目标节点时,服务只调用route方法,算法对象从哪里来、如何构造,都由工厂配置决定。这也为运行时切换算法打下基础,因为只要所有算法实现都满足同一个接口,替换就只是更换实例。
用映射类型和判别联合表达动态算法选择
动态选择算法的第一步是把可选算法枚举出来。直接用boolean或number表示算法标识没有任何可读性,而且容易传错值。字符串字面量联合类型既可以获得自动补全,又能在switch语句中做穷举检查。下面先定义一个AlgorithmOptionMap,再把算法标识推导为它的键集合。每种算法的参数被单独定义成接口,避免出现options: any这种模糊配置。
interface AStarOptions {
heuristicWeight: number;
gridSize: number;
}
interface JpsOptions {
gridSize: number;
allowDiagonal: boolean;
}
interface VisibilityOptions {
margin: number;
}
interface GridOptions {
gridSize: number;
penalty: number;
}
interface AlgorithmOptionMap {
astar: AStarOptions;
jps: JpsOptions;
visibility: VisibilityOptions;
grid: GridOptions;
}
type AlgorithmKind = keyof AlgorithmOptionMap;
当AlgorithmKind来自keyof AlgorithmOptionMap时,增加一种新算法只需要在映射接口中增加一个字段,所有依赖该联合类型的位置都会自动获得新成员。这比手写联合类型更利于后续扩展。接下来定义配置对象,使用映射类型构造判别联合,让kind字段成为判别属性,从而在条件分支中自动收窄options类型。
type RoutingConfig = {
[K in AlgorithmKind]: {
kind: K;
options: AlgorithmOptionMap[K];
};
}[AlgorithmKind];
function isRoutingConfig<K extends AlgorithmKind>(
config: RoutingConfig | AlgorithmKind,
kind: K
): config is Extract<RoutingConfig, { kind: K }> {
return typeof config === 'object' && config.kind === kind;
}
RoutingConfig是一个判别联合,当你传入一个对象时,TypeScript能根据kind的值把options收窄到具体类型。例如在if (config.kind === 'astar')分支中,config.options.heuristicWeight可以直接访问,不会报错。如果配置写成kind: 'astar', options: { margin: 2 },编译器会立即提示margin不是AStarOptions的属性。这种检查把大量运行时错误提前到了编译期。isRoutingConfig这个类型守卫进一步把传入值从联合类型中精确提取,让调用方无需手动断言。
构建可扩展的算法注册表与模块增强
光有配置类型还不够,程序运行时还需要根据kind构造出对应的算法实例。注册表模式可以将字符串标识映射到构造函数,添加新算法时只改注册表,不改业务调用代码。注册表本身也可以用接口约束,避免值类型意外丢失。
interface AlgorithmConstructor {
new (options?: unknown): RoutingAlgorithm;
}
interface AlgorithmRegistry {
astar: AlgorithmConstructor;
jps: AlgorithmConstructor;
visibility: AlgorithmConstructor;
grid: AlgorithmConstructor;
}
const registry: AlgorithmRegistry = {
astar: AStarRouter,
jps: JpsRouter,
visibility: VisibilityRouter,
grid: GridRouter,
};
function createRouter<K extends AlgorithmKind>(
kind: K,
options: AlgorithmOptionMap[K]
): RoutingAlgorithm {
return new registry[kind](options);
}
这里createRouter的泛型K extends AlgorithmKind保证了kind和options之间的关联。即使registry对象中的构造函数都接收unknown,调用方传入的options仍然被精确校验。注意AlgorithmConstructor使用unknown而不是any,这样可以强制每个构造函数内部自行处理参数类型,避免因为any扩散导致类型检查失效。如果某个构造函数内部错误地把unknown直接当具体类型使用,编译器会给出警告,而不是静默放过。
对于需要支持第三方插件或独立包扩展算法的场景,TypeScript的接口声明合并非常有用。假设核心包导出了AlgorithmOptionMap和AlgorithmRegistry,扩展包可以在自己的声明文件中重新打开这两个接口,增加custom字段。由于AlgorithmKind是keyof AlgorithmOptionMap,扩展后联合类型自动包含custom,不需要修改核心包源码。这样既保持核心库稳定,又允许业务侧无缝接入自定义避障算法。
// 扩展包中的声明文件
declare module './routing-types' {
interface AlgorithmOptionMap {
custom: CustomOptions;
}
interface AlgorithmRegistry {
custom: CustomRouterConstructor;
}
}
这种模块增强方式非常灵活,插件作者无需了解核心路由器的实现细节,只需提供符合接口约定的构造函数和配置类型。扩展后的AlgorithmKind会自动变为astar、jps、visibility、grid、custom的联合类型,所有使用该联合类型的函数都能获得新算法的自动补全和穷举检查。如果某个switch语句遗漏了custom,打开strict类型检查后会提示未覆盖所有情况,从而避免漏处理。
根据障碍密度动态推荐算法并保持类型约束
动态避障不仅是用户可以手动选择算法,也可以是系统根据当前画布状态自动推荐。障碍物密度是一个常用启发式指标:当节点覆盖面积占画布比例较高时,可见性图算法通常能得到更短的路径;节点数量很多但密度较低时,跳点搜索在栅格上的扩展效率更高。这个推荐函数返回AlgorithmKind,使得下游直接进入类型安全的分发流程。
function selectAlgorithm(ctx: AvoidanceContext): AlgorithmKind {
const density = ctx.obstacles.reduce(
(sum, rect) => sum + rect.width * rect.height,
0
) / (ctx.bounds.width * ctx.bounds.height);
if (density > 0.6) {
return 'visibility';
}
if (ctx.obstacles.length > 50) {
return 'jps';
}
return 'astar';
}
const recommendedKind = selectAlgorithm(currentContext);
const router = createRouter(recommendedKind, getOptionsFor(recommendedKind));
const route = router.route(startPoint, endPoint, currentContext);
selectAlgorithm返回类型AlgorithmKind,因此调用方可以把结果传给createRouter,编译期确认返回值一定在注册表中。推荐逻辑还可以进一步做成可配置的策略:把密度阈值、算法优先级放在一个RecommendationRule数组中,根据规则匹配返回第一个命中的算法。规则数组的类型可以定义为Array<RecommendationRule>,每一项包含test函数和kind字段,从而避免散落的if-else。这种策略数组可以在编辑器配置面板中修改,而类型签名保持不变。
interface RecommendationRule {
kind: AlgorithmKind;
test(ctx: AvoidanceContext): boolean;
}
const rules: Array<RecommendationRule> = [
{ kind: 'visibility', test: (ctx) => computeDensity(ctx) > 0.6 },
{ kind: 'jps', test: (ctx) => ctx.obstacles.length > 50 },
{ kind: 'astar', test: () => true },
];
function recommend(rules: Array<RecommendationRule>, ctx: AvoidanceContext): AlgorithmKind {
const matched = rules.find((rule) => rule.test(ctx));
return matched ? matched.kind : 'astar';
}
当这些类型组合在一起,流程图编辑器就能在不牺牲开发体验的前提下,根据节点数量、密度、用户偏好、插件注册情况动态切换正交路由避障算法。核心类型文件通常只有一百多行,却覆盖了从几何建模到算法分发的所有关键约束。整个类型体系没有依赖具体图形框架,任何接收起点、终点和障碍物集合的路由实现都可以接入,后续扩展也不会破坏现有代码的类型安全。
TypeScript正交路由避障算法修改时间:2026-09-19 00:50:17