TypeScript严格模式下unknown类型转指定对象类型的问题
在TypeScript严格模式下解析JSON得到unknown类型的值,定义了如下类型:
type Auth = { accessToken: string; };
需要实现一个同时保证运行时校验和编译时类型安全的转换函数,写了以下代码:
type Auth = { accessToken: string; }; function convert(value: unknown): Auth { if (typeof value != "object") { throw new Error(`Unexpected type: ${typeof value}`); } if (value == null) { throw new Error("Unexpected value: null"); } if (!("accessToken" in value)) { throw new Error("Missing property: accessToken"); } if (typeof value.accessToken != "string") { throw new Error(`Unexpected type: ${typeof value.accessToken}`); } return value; }
但编译时出现错误:
Type 'object & Record<"accessToken", unknown>' is not assignable to type 'Auth'. Types of property 'accessToken' are incompatible. Type 'unknown' is not assignable to type 'string'.ts(2322)
不想用value as Auth强制断言,因为后续给Auth新增字段时,编译器没法提醒补充对应的校验逻辑,希望不绕过类型系统实现转换。
方法一:自定义类型守卫函数
把校验逻辑抽成一个带类型谓词的函数,既能做运行时检查,又能让TypeScript正确收窄类型:
type Auth = { accessToken: string; }; function isAuth(value: unknown): value is Auth { if (typeof value !== "object" || value === null) { return false; } const authCandidate = value as Record<string, unknown>; return "accessToken" in authCandidate && typeof authCandidate.accessToken === "string"; } function convert(value: unknown): Auth { if (!isAuth(value)) { throw new Error("Invalid Auth structure"); } return value; }
这里的value is Auth类型谓词会告诉TypeScript:如果isAuth返回true,那么value一定符合Auth类型。后续如果Auth新增字段,比如加refreshToken: string,修改isAuth时必须同步添加对应校验,否则类型系统会在使用转换后的Auth对象时保持安全,而且你也能明确知道要补充检查逻辑。
方法二:函数内逐步收窄类型
不想拆分函数的话,可以用临时变量辅助TypeScript识别属性,再通过校验逐步收窄类型:
type Auth = { accessToken: string; }; function convert(value: unknown): Auth { if (typeof value !== "object" || value === null) { throw new Error(`Unexpected type: ${typeof value}`); } // 先把value断言为包含可选accessToken的对象,辅助TS识别属性 const candidate = value as { accessToken?: unknown }; if (!("accessToken" in candidate)) { throw new Error("Missing property: accessToken"); } if (typeof candidate.accessToken !== "string") { throw new Error(`Unexpected type for accessToken: ${typeof candidate.accessToken}`); } // 此时candidate的结构完全匹配Auth,断言是安全的 return candidate as Auth; }
这种方式下,后续如果Auth新增字段,比如refreshToken: string,返回时TypeScript会因为candidate没有这个属性而报错,你就能立刻知道要添加对应的检查逻辑,不会漏掉校验。
方法三:使用断言函数(TS 3.7+)
TypeScript 3.7及以上支持断言函数,可以直接在函数内告诉编译器类型:
type Auth = { accessToken: string; }; // 断言函数:如果不抛出错误,value就是Auth类型 function assertIsAuth(value: unknown): asserts value is Auth { if (typeof value !== "object" || value === null) { throw new Error(`Unexpected type: ${typeof value}`); } if (!("accessToken" in value)) { throw new Error("Missing property: accessToken"); } if (typeof value.accessToken !== "string") { throw new Error(`Unexpected type for accessToken: ${typeof value.accessToken}`); } } function convert(value: unknown): Auth { assertIsAuth(value); return value; }
断言函数的语义更明确,专门用于“校验不通过就抛错”的场景。后续修改Auth类型时,必须同步更新assertIsAuth里的校验逻辑,否则编译器会在使用value为Auth类型时检测到不匹配,提醒你补充检查。
内容的提问来源于stack exchange,提问作者vbezhenar

