06 函数返回值与“不是所有路径都有 return”

你的问题不是 return 写在 if 里不合法,而是函数承诺了返回值,某些路径却可能走到结尾。

1. 先把所有路径写出来

function statusLabel(status: string): string {
  if (status === "active") return "已启用";
  if (status === "banned") return "已禁用";
} // 错误:其他字符串会走到结尾,实际返回 undefined

status: string 可以是 pending、empty 或任何字符串。函数结尾没 return 时,JS 会返回 undefined,与声明的 string 冲突。

active → 返回已启用
banned → 返回已禁用
其他值 → 到达结尾 → undefined

TS 关心路径是否完整,不关心 return 是放在 if 里面还是外面。

2. 三种常见解法,业务含义不同

默认返回:允许未知状态

function statusLabel(status: string): string {
  if (status === "active") return "已启用";
  if (status === "banned") return "已禁用";
  return "未知状态";
}

适合展示兜底,但不适合把非法写入输入静默当成正常。

抛出异常:不允许未知状态

function statusLabel(status: string): string {
  if (status === "active") return "已启用";
  if (status === "banned") return "已禁用";
  throw new Error("不支持的状态");
}

抛异常的路径没有正常返回,因此不需要返回 string。接入当前工程时,按错误是否可公开选择 AppError 或内部 Error。

明确允许无结果

function statusLabel(status: string): string | undefined {
  if (status === "active") return "已启用";
  if (status === "banned") return "已禁用";
  return undefined;
}

调用者必须处理 undefined。不要仅为了消红线就扩大返回类型;只有“无结果”合理时才这样设计。

3. 独立 if 不一定证明所有有限分支已覆盖

type Status = "active" | "banned";

function statusLabel(status: Status): string {
  if (status === "active") return "已启用";
  if (status === "banned") return "已禁用";
  // 即使人能判断两个值已列完,TS 对这种路径分析也可能仍要求结束分支。
}

可以使用 if/else,或完整 switch,让控制流更明确:

function statusLabel(status: Status): string {
  if (status === "active") return "已启用";
  else return "已禁用";
}

但以后加第三种状态,else 会把它误当 banned。状态集合会扩展时,穷尽 switch 更安全。

4. never 与穷尽检查

type Status = "pending" | "active" | "banned";

function assertNever(value: never): never {
  throw new Error(`未处理的状态:${String(value)}`);
}

function statusLabel(status: Status): string {
  switch (status) {
    case "pending": return "待处理";
    case "active": return "已启用";
    case "banned": return "已禁用";
    default: return assertNever(status);
  }
}

全部可能值被排除后,剩余类型是 never。若新增 suspended,却没加对应 case,default 的 status 不再是 never,编译器会提醒。

never 表示不可能出现的值/正常完成路径;assertNever 会真实抛异常,但状态类型本身不会验证外部请求。输入仍需先校验。

5. void、undefined、never 不一样

类型 意义 示例
void 调用方不应依赖返回值 日志函数、发送响应的 Controller
undefined 正常返回的值就是 undefined 明确无结果的分支
never 不会正常完成并返回 总是抛异常的函数

一个上下文要求 () => void 的回调,TS 可以允许它实际返回某个值,但调用方忽略它;这与声明 function f(): void { return 123; } 不同,后者通常报错。

const callback: () => void = () => 123; // 允许,返回值被 void 契约忽略

这不是让你在 Controller 里随意返回业务数据;Controller 应明确调用 res.json,再结束。

6. async 函数与 catch 也属于返回路径

async function loadName(): Promise<string> {
  try {
    return await Promise.resolve("ANN");
  } catch (error: unknown) {
    console.error(error);
    // 错误:catch 路径正常结束,没有 string
  }
}

如果失败应该传播,就 throw error;如果允许降级,返回真实的默认值;如果失败也是业务结果,定义明确的结果联合。不能只记日志然后把失败变成隐式 undefined。

7. return 只结束它所在的函数

function findName(names: string[]): string {
  names.forEach((name) => {
    if (name.startsWith("A")) return name;
  });
  // 回调 return 没有让 findName 返回。
}

用 for…of 直接返回,或使用 find 并处理无结果:

function findName(names: string[]): string | undefined {
  return names.find((name) => name.startsWith("A"));
}

Promise 回调中的 return 同样需要通过外层 return/await 链接;具体见 09 章。

8. strictNullChecks 与 noImplicitReturns 的区别

当前工程 strict: true,通常包含 strictNullChecks,因此明确返回 string 的函数不能意外返回 undefined。

noImplicitReturns 当前注释掉,没有启用。它不是 strict 自动包含的检查;开启后会额外检查返回路径,例如一些推断为 string | undefined 的函数也可能被提示。

所以“当前没开 noImplicitReturns”不意味着“漏 return 就永远不报错”。本系列解释现有配置,不修改 tsconfig。

9. Service 与 Controller 的建议

Service 的公共返回契约值得显式保留,例如 Promise<CustomerDetailDto>,能及时发现字段遗漏与 catch 吞错。

Controller 常使用 Promise<void>:等待 Service,发送响应,然后结束。早退出写成:

res.json(result);
return;

这里 result/res 来自 Controller 上下文。不要误写 return res.json(result),再困惑为何返回 Response 与 Promise<void> 不兼容。

10. 练习

  1. 分别用默认值、抛错和可选返回处理未知状态,说出三种业务差别。
  2. 新增一个状态,验证 assertNever 是否提醒漏处理。
  3. 找一个 catch 只记录日志的函数,判断它会正常返回什么。
  4. 把 forEach 中的 return 改成 for…of,观察外层返回行为。

下一章:类型收窄与守卫。