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. 练习
- 分别用默认值、抛错和可选返回处理未知状态,说出三种业务差别。
- 新增一个状态,验证 assertNever 是否提醒漏处理。
- 找一个 catch 只记录日志的函数,判断它会正常返回什么。
- 把 forEach 中的 return 改成 for…of,观察外层返回行为。
下一章:类型收窄与守卫。