Three Frontend Patterns for Date Tools That Depend on Rules

Three Frontend Patterns for Date Tools That Depend on Rules

三种针对依赖规则的日期工具的前端模式

A normal date widget can add a duration and display the result. A policy-dependent date tool has a different job: it must show when the inputs are incomplete, which version of a rule was applied, and which conclusion the software cannot make. Here are three TypeScript patterns that keep those boundaries visible.

普通的日期组件只需执行加法并显示结果即可。但依赖政策的日期工具任务不同:它必须在输入不完整时进行提示,明确应用了哪个版本的规则,并指出软件无法得出的结论。以下是三种能让这些边界清晰可见的 TypeScript 模式。

1. Replace eligible: boolean with a state union

1. 用状态联合类型(State Union)替代 eligible: boolean

A Boolean makes every incomplete case look like a negative decision. Use a discriminated union instead:

布尔值会让所有不完整的情况看起来都像是“否定”的决定。请改用判别联合类型(Discriminated Union):

type ResultState = 
  | { kind: 'needs-route' }
  | { kind: 'needs-evidence'; fields: string[] }
  | { kind: 'estimate'; date: string; assumptions: string[] }
  | { kind: 'possible-issue'; checks: string[] }
  | { kind: 'outside-scope'; reasons: string[] };

The component can now render a specific next step. More importantly, the domain layer cannot quietly convert missing information into false.

现在,组件可以渲染具体的下一步操作。更重要的是,领域层无法再将缺失的信息悄悄转换为“假”(false)。

function Result({ state }: { state: ResultState }) {
  switch (state.kind) {
    case 'needs-route': return <Notice>Select the route before calculating a date.</Notice>;
    case 'needs-evidence': return <Notice>Check: {state.fields.join(', ')}</Notice>;
    case 'estimate': return <Estimate date={state.date} assumptions={state.assumptions} />;
    case 'possible-issue': return <Warning checks={state.checks} />;
    case 'outside-scope': return <Boundary reasons={state.reasons} />;
  }
}

2. Store rule versions with provenance

2. 存储带有来源信息的规则版本

Avoid anonymous constants such as MAX_DAYS = 180. A policy rule needs an effective date, source, review date, and scope.

避免使用像 MAX_DAYS = 180 这样的匿名常量。政策规则需要包含生效日期、来源、审查日期和适用范围。

type RuleVersion<T> = {
  id: string;
  appliesFrom: string;
  appliesTo?: string;
  routes: string[];
  sourceUrl: string;
  reviewedAt: string;
  evaluate: (facts: T) => RuleCheck;
};

When a rule changes, add a version. Do not replace the old object and silently change the meaning of stored results. Regression tests should cover a date on each side of the boundary.

当规则发生变化时,请添加一个新版本。不要直接替换旧对象,从而悄悄改变已存储结果的含义。回归测试应覆盖边界两侧的日期。

it('selects the version active for the relevant period', () => {
  expect(selectRule('2026-01-01').id).toBe('rule-v2');
});

3. Generate the explanation from the evaluation object

3. 从评估对象生成解释

Do not calculate a date in one function and write a reassuring paragraph somewhere else. Return the information needed to explain the result:

不要在一个函数中计算日期,然后在别处编写一段解释性文字。应返回解释结果所需的所有信息:

type Evaluation = {
  state: ResultState;
  route: string;
  ruleIds: string[];
  sources: string[];
  inputWarnings: string[];
};

The UI can show the route, rule versions, input warnings, and source links beside the date. If the result uses two historic branches, that should be visible without reading logs.

UI 可以在日期旁边显示路径、规则版本、输入警告和来源链接。如果结果使用了两个历史分支,用户无需查看日志也应能直观看到。

Preserve chronology and uncertainty

保留时间顺序与不确定性

Travel, employment, or coverage records should remain event-level data. Do not discard the original entries after producing an annual total. Keep the evidence source and confidence level so corrections are recoverable.

旅行、就业或保险记录应保留为事件级数据。在计算出年度总计后,不要丢弃原始条目。保留证据来源和置信度,以便在需要时进行修正。

type Interval = {
  start: string;
  end: string;
  source: 'document' | 'calendar' | 'memory';
  confidence: 'confirmed' | 'estimated';
};

Normalise overlaps in a derived view, then test invariants: sorting does not change the total, duplicates do not double-count, and reversed dates fail early.

在派生视图中归一化重叠部分,然后测试不变性:排序不应改变总数,重复项不应被重复计算,日期倒置应尽早报错。

A public example

一个公开案例

The ILR Calculator UK methodology and interface applies these ideas to settlement planning: route selection precedes the date, trip records stay in the browser, and the output is labelled as an estimate rather than an eligibility decision. The link is an implementation example, not a claim that the product or this post can assess an individual application.

英国 ILR 计算器的方法论和界面将这些理念应用于定居规划:路径选择先于日期计算,行程记录保留在浏览器中,输出结果被标记为“估算值”而非“资格判定”。该链接仅作为实现示例,并不代表该产品或本文能够评估个人申请。

These patterns are reusable in tax-residency tools, insurance waiting periods, employment-benefit calculators, and any frontend where rules change over time. The best result component is not the one that always produces a date. It is the one that can explain when a date is justified and when the software should stop.

这些模式可复用于税务居民身份工具、保险等待期计算、就业福利计算器,以及任何规则随时间变化的各种前端场景。最好的结果组件不是那种总是能算出日期的组件,而是那种能够解释“何时日期是合理的”以及“软件何时应该停止计算”的组件。