跳到主要内容

如何把 unknown 指定为更具体的类型?

下文默认基于 TypeScript 5.x,并假设开启 strict: true

面试速答(30 秒版 TL;DR)

  • unknown 不能直接当成具体类型使用,必须先做类型收窄(type narrowing)
  • 最常见的收窄方式有:
    • typeof
    • instanceof
    • Array.isArray
    • in
    • 自定义类型守卫 value is Xxx
    • 必要时使用类型断言 as Xxx
  • 如果数据来自外部系统,最稳的做法不是直接 as,而是先校验再收窄。

心智模型:先证明,再使用

1. 用 typeof 收窄基础类型

function print(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase());
}

if (typeof value === "number") {
console.log(value.toFixed(2));
}
}

适用范围:

  • string
  • number
  • boolean
  • bigint
  • symbol
  • undefined
  • function

注意:

  • typeof null === "object",不能靠它单独判断普通对象。

2. 用 instanceof 收窄类实例

function format(value: unknown) {
if (value instanceof Date) {
return value.toISOString();
}

if (value instanceof Error) {
return value.message;
}

return String(value);
}

适合:

  • Date
  • Error
  • 自定义 class 实例

不适合:

  • 普通对象字面量结构判断

3. 用 Array.isArray 判断数组

function first(value: unknown) {
if (Array.isArray(value)) {
return value[0];
}

return undefined;
}

这是数组判断最稳的入口,不要用 typeof value === "object" 代替。

4. 用 in 判断对象上是否存在某个字段

function readName(value: unknown) {
if (
typeof value === "object" &&
value !== null &&
"name" in value
) {
return value.name;
}

return undefined;
}

这里必须先判断:

  • typeof value === "object"
  • value !== null

否则 innull 或原始值会出问题。

5. 用自定义类型守卫做可复用收窄

这是工程里最常用、也最值得面试重点说的方式。

type User = {
id: number;
name: string;
};

function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "number" &&
"name" in value &&
typeof value.name === "string"
);
}

function greet(value: unknown) {
if (isUser(value)) {
return `hello, ${value.name}`;
}

return "invalid user";
}

关键点:

  • 返回值写成 value is User
  • 这不是运行时新类型,而是告诉编译器“如果返回 true,这里就按 User 看”。

6. 类型断言 as 可以用,但不是首选

const value: unknown = JSON.parse('{"name":"Tom"}');
const user = value as { name: string };

这叫类型断言,不叫验证。

也就是说:

  • 编译器会相信你;
  • 运行时不会帮你检查真假。

因此面试里最好这样表述:

  • as 适合你已经非常确定数据来源;
  • 边界输入更推荐“校验 + 收窄”,而不是“盲断言”。

7. 实战建议:边界层做一次收窄,不要到处散着写

不推荐:

const data: unknown = await fetchSomething();
// 后面每个地方都在 as

更推荐:

type ApiUser = {
id: number;
name: string;
};

function assertApiUser(value: unknown): asserts value is ApiUser {
if (
typeof value !== "object" ||
value === null ||
!("id" in value) ||
typeof value.id !== "number" ||
!("name" in value) ||
typeof value.name !== "string"
) {
throw new Error("invalid api user");
}
}

然后在入口统一校验,后面业务代码就能稳定使用具体类型。

8. 高频面试追问

8.1 unknownas 一起用是不是就够了

不够。

因为 as 只是告诉编译器你相信它,不是真校验。

8.2 收窄和断言有什么区别

  • 收窄:通过代码分支让编译器推导出更具体类型;
  • 断言:开发者直接指定“把它当成这个类型”。

收窄更安全,断言更冒险。

8.3 为什么很多库会引入 zod / valibot / io-ts

因为当对象结构很复杂时,手写守卫容易:

  • 重复;
  • 漏字段;
  • 难维护。

这类库本质上是在做“运行时校验 + 静态类型联动”。

速记要点

  • unknown 变具体类型的核心不是 as,而是收窄。
  • 常用手段:typeofinstanceofArray.isArrayin、自定义守卫。
  • 外部输入优先“校验后收窄”,不要直接盲断言。