类型断言、非空断言、as unknown as 和 satisfies 有什么区别?
类型断言 as T 是告诉编译器“按 T 来看这个值”,只在两种类型有关联(一方更具体或更宽泛)时才允许,编译后被删除,没有运行时检查。非空断言 x! 是去掉 null 和 undefined 的断言。as unknown as T 先转成 unknown 再转成 T,绕过“类型要有关联”的限制,几乎什么都能转。satisfies 不改变值的类型,只检查值能不能赋给目标类型,写错会报错,所以比断言安全。
简答
比如
document.getElementById('main_canvas')只能推断出HTMLElement | null,可你知道页面上那个元素一定是画布,就可以写as HTMLCanvasElement,让编译器按画布的类型来检查后面的代码。这就是类型断言:你比编译器多知道一点信息,直接告诉它。断言只影响编译期,编译后会被删掉,运行时没有任何检查,断言错了也不会抛错或者变成
null。所以 TypeScript 给断言设了一道限制:只能把类型转成更具体或更宽泛的版本,'hello' as number这种两边毫无关系的会报错。非空断言x!是一种特殊的断言,表示“这个值不是null也不是undefined”,同样没有运行时检查。如果确实要在两个没关系的类型之间转,可以先转成
unknown或any再转成目标类型,也就是as unknown as T。因为任何类型都能转成unknown,unknown也能转成任何类型,这道限制就被绕过去了。它等于完全放弃了检查,只适合测试替身这类明确知道在做什么的场合。
satisfies和它们方向相反。value satisfies T不改变值的类型,只检查value能不能赋给T,少写必填属性、拼错键名都会报错。所以想“确认值符合某个类型”时用satisfies或类型注解,不要用as。
断言改的是编译器的看法
下面这段代码能通过 tsc --strict(用 TypeScript 7.0.2 验证)。看每一处断言,编译器检查了什么、没检查什么。
type User = { id: number; name: string };
const raw: unknown = JSON.parse('{"id":1,"name":"Ada"}');
const user = raw as User;
user.name.toUpperCase();
// @ts-expect-error 2352:string 和 number 没有足够的重叠
const n = 'hello' as number;
const forced = 'hello' as unknown as number;
const partial = {} as User;
raw as User 能通过,是因为 unknown 可以转成任何类型。可编译器并没有检查 JSON 里真的有 name,如果接口返回的数据少了这个字段,user.name.toUpperCase() 会在运行时报错。外部数据要在运行时校验,见《为什么 TypeScript 不能替代接口数据校验?》。
'hello' as number 报的是 TS2352,错误信息说两个类型“没有足够的重叠”,建议先转成 unknown。这就是 Handbook 里说的规则:断言只允许转成更具体或更宽泛的版本,用来拦住明显不可能的转换。
as unknown as number 照着错误信息的建议做了,于是不再报错。可 forced 运行时仍然是字符串,调用 toFixed 就会出错。
{} as User 也能通过,因为 User 比 {} 更具体,属于允许的方向。可这个对象既没有 id 也没有 name,编译器不会提醒。这是断言最常见的误用:本来想表达“这个对象是 User”,结果把必填属性的检查也跳过了。
非空断言 x!
function len(text?: string | null) {
return text!.length;
}
text 的类型是 string | null | undefined,直接读 .length 会报错。加上 ! 以后,编译器把它当成 string。Handbook 说明,! 和其他断言一样不改变运行时行为,所以只有确定值不可能是 null 或 undefined 时才该用。调用 len() 不传参数,运行时照样会报错。
能用判断代替时,优先用判断或可选链,比如 text?.length ?? 0。判断之后编译器会自动缩小类型,见《TypeScript 怎样通过控制流缩小类型?》。
satisfies 检查而不改写
type Config = { port: number; host?: string };
const config = { port: 3000 } satisfies Config;
config.port.toFixed();
// @ts-expect-error 拼错的键会被 satisfies 拦下
const bad = { prot: 3000 } satisfies Config;
const asserted = { host: 'localhost' } as Config;
asserted.port.toFixed();
satisfies 把 prot 拼错的对象拦了下来。换成 as Config,缺少 port 的对象照样能通过,asserted.port 的类型是 number,可运行时它是 undefined。
所以两者的方向不同:as 是“我说它是这个类型,你别管”,satisfies 是“请帮我检查它符合这个类型”。satisfies 和类型注解、as const 的更多对比见《as const、类型注解和 satisfies 有什么不同?》。
常见追问
尖括号写法 <T>value 和 value as T 有区别吗?
两者等价。但尖括号在
.tsx文件里会和 JSX 冲突,所以不能用,日常统一写as更省事。
什么时候用断言是合理的?
编译器确实缺信息、而你能保证正确的时候。比如通过 ID 拿到一个确定是画布的 DOM 元素,或者在单元测试里构造一个只实现了部分方法的替身对象。接口返回的数据、用户输入这类没法保证的值,应该做运行时校验,而不是断言。
any 和 as unknown as 有什么不同?
as unknown as T只在这一处放弃检查,结果是一个确定的类型T,后面的代码照常检查。any会一路传染,从它取出的属性、调用的返回值也都是any,检查一直被关掉。两者都应少用,详见《any、unknown 和 never 分别适合什么场景?》。