TypeScript编译通过了,接口数据为什么还会出错
接口文档写着 id 是数字,页面代码也使用了明确的 TypeScript 类型,但线上仍然出现了“无法读取空值属性”。检查响应后才发现,某个异常分支返回了 null,另一个字段则从数字变成了字符串。
TypeScript 能检查代码里的类型关系,编译成 JavaScript 后,大部分类型信息已经被移除。服务器响应、浏览器存储和用户输入不会因为前端声明了一个接口,就自动符合这个接口。
下面这段写法很常见,却没有验证真实数据:
interface User {
id: number;
name: string;
}
const user = await response.json() as User;
as User 的含义是告诉编译器“请按这个类型理解”,它不会把字符串转换成数字,也不会补齐缺失字段。给请求函数加一个泛型,通常也只是改善静态提示,不能凭空获得运行时检查。
对于结构简单的响应,可以先把外部数据当作 unknown,再用类型守卫判断。类型守卫本质上是一个返回布尔值的函数,只是它还向编译器说明:检查通过后,这个值可以按什么类型使用。
interface User {
id: number;
name: string;
email?: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object'
&& value !== null
&& !Array.isArray(value);
}
function isUser(value: unknown): value is User {
if (!isRecord(value)) return false;
return typeof value.id === 'number'
&& Number.isSafeInteger(value.id)
&& value.id > 0
&& typeof value.name === 'string'
&& (value.email === undefined || typeof value.email === 'string');
}
async function loadUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${encodeURIComponent(id)}`);
if (!response.ok) throw new Error('用户信息请求失败');
const value: unknown = await response.json();
if (!isUser(value)) throw new Error('用户信息格式不符合约定');
return value;
}
这段检查明确规定:编号必须是正的安全整数,名字必须是字符串,邮箱可以缺省。邮箱目前只检查了类型,没有验证地址格式;名字也允许空字符串。业务如果有额外要求,应在这里补上,不能因为函数叫 isUser 就默认它已经覆盖所有规则。
还要注意,JavaScript 里的 typeof null 会得到 object,因此对象检查必须排除空值。数组同样属于对象,示例也把它排除了。对于数字,单纯检查 typeof 还会放过 NaN 等特殊值,所以要根据业务选择有限数字、安全整数或其他更具体的条件。
校验通过之后,程序可以在内部沿用稳定的类型,不必每个组件都重复判断。合适的位置通常是请求层或数据适配层:外部数据刚进入应用时检查一次,把格式错误转成明确的失败状态。如果在数据深处才处理,错误可能已经传播到多个页面。
这个守卫允许出现额外字段。这对接口增加兼容字段比较友好,但如果数据用于生成签名、写入数据库或控制权限,可能还需要明确挑选允许使用的字段。通过校验也不等于可信授权,账号权限仍然必须由服务端验证。
当响应变得很深,包含多种联合结构和大量重复规则时,手写守卫容易遗漏。此时可以评估项目已有的校验库或从接口定义生成校验器,但不用为了两个简单字段立即增加新依赖。无论采用什么工具,核心都是把“声明类型”和“检查真实值”分别完成。
验证时,可以准备正常对象、null、空对象、字符串编号、负数编号和缺失可选字段这几组输入。确认错误数据在边界被拒绝,正确数据能够继续使用。比起把报错点强制断言为正确类型,这样做能让接口变化更早暴露,也更容易定位。
- 点赞
- 收藏
- 关注作者
评论(0)