跳到主要内容

类型断言

问题

什么是 TypeScript 类型断言?as<>!as const 分别有什么作用?

答案

类型断言是告诉 TypeScript 编译器"相信我,我知道这个值的类型"。它不会改变运行时行为,只是在编译时覆盖类型推断。


基本语法

as 语法(推荐)

// 将 unknown 断言为具体类型
const value: unknown = 'hello';
const str = value as string;
console.log(str.toUpperCase()); // HELLO

// 从联合类型收窄
const input: string | number = 'hello';
const length = (input as string).length;

// DOM 元素断言
const element = document.getElementById('input') as HTMLInputElement;
element.value = 'Hello';

尖括号语法

// 等价于 as 语法
const value: unknown = 'hello';
const str = <string>value;

// 注意:在 JSX 中不能使用,会与 JSX 标签冲突
// const element = <HTMLInputElement>document.getElementById('input'); // ❌ 在 JSX 中报错
推荐

优先使用 as 语法,因为它在 JSX 中也能正常工作。


非空断言(!)

告诉编译器值一定不是 nullundefined

// DOM 元素可能为 null
const element = document.getElementById('app');
// element.innerHTML = 'Hello'; // 错误:element 可能为 null

// 使用非空断言
const element2 = document.getElementById('app')!;
element2.innerHTML = 'Hello'; // OK

// 可选属性
interface User {
name?: string;
}

const user: User = { name: 'Alice' };
const length = user.name!.length; // 断言 name 一定存在

// 明确赋值断言
class Example {
name!: string; // 告诉编译器会在其他地方初始化

initialize(name: string) {
this.name = name;
}
}
危险

非空断言会跳过类型检查。如果值实际为 null/undefined,运行时会报错。 优先使用类型守卫或可选链:

// 更安全的方式
const element = document.getElementById('app');
if (element) {
element.innerHTML = 'Hello';
}

// 或使用可选链
const length = user.name?.length ?? 0;

const 断言(as const)

将值断言为字面量类型,使其变为只读且不可变:

// 不使用 as const
const config = {
url: 'https://api.example.com',
method: 'GET'
};
// 类型:{ url: string; method: string }

// 使用 as const
const configConst = {
url: 'https://api.example.com',
method: 'GET'
} as const;
// 类型:{ readonly url: "https://api.example.com"; readonly method: "GET" }

// 数组
const arr = [1, 2, 3]; // number[]
const arrConst = [1, 2, 3] as const; // readonly [1, 2, 3]

// 字符串字面量
let str = 'hello'; // string
const strConst = 'hello' as const; // "hello"

实际应用

// 创建枚举替代
const HttpMethods = ['GET', 'POST', 'PUT', 'DELETE'] as const;
type HttpMethod = typeof HttpMethods[number]; // "GET" | "POST" | "PUT" | "DELETE"

// 对象键
const Colors = {
Red: '#ff0000',
Green: '#00ff00',
Blue: '#0000ff'
} as const;

type ColorName = keyof typeof Colors; // "Red" | "Green" | "Blue"
type ColorValue = typeof Colors[ColorName]; // "#ff0000" | "#00ff00" | "#0000ff"

// 路由配置
const routes = [
{ path: '/', component: 'Home' },
{ path: '/about', component: 'About' },
{ path: '/contact', component: 'Contact' }
] as const;

type RoutePath = typeof routes[number]['path']; // "/" | "/about" | "/contact"

双重断言

当直接断言不被允许时,可以先断言为 unknown

// 直接断言可能不被允许
interface Cat {
meow(): void;
}

interface Dog {
bark(): void;
}

const cat: Cat = { meow: () => {} };
// const dog = cat as Dog; // 错误:Cat 和 Dog 之间没有足够的重叠

// 双重断言
const dog = cat as unknown as Dog; // OK(但危险!)

// 或使用 any
const dog2 = cat as any as Dog;
警告

双重断言非常危险,基本上绕过了所有类型检查。只在极端情况下使用,并确保你知道自己在做什么。


类型断言 vs 类型守卫

interface User {
name: string;
email: string;
}

// ❌ 类型断言:不安全,运行时不检查
function processUserUnsafe(data: unknown) {
const user = data as User; // 编译器信任你,但data可能不是User
console.log(user.name); // 运行时可能报错
}

// ✅ 类型守卫:安全,运行时检查
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
'email' in data
);
}

function processUserSafe(data: unknown) {
if (isUser(data)) {
console.log(data.name); // 安全
} else {
console.log('Invalid user data');
}
}

断言函数

使用 asserts 关键字定义断言函数:

function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
}

function process(value: unknown) {
assertIsString(value);
// 此后 value 类型为 string
console.log(value.toUpperCase());
}

// 断言非空
function assertDefined<T>(
value: T,
message?: string
): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error(message ?? 'Value must be defined');
}
}

// 通用断言函数
function assert(
condition: boolean,
message?: string
): asserts condition is true {
if (!condition) {
throw new Error(message ?? 'Assertion failed');
}
}

function divide(a: number, b: number) {
assert(b !== 0, 'Cannot divide by zero');
return a / b;
}

satisfies 操作符(TypeScript 4.9+)

satisfies 既能保持推断的精确类型,又能确保满足某个约束:

// 使用 as:丢失精确类型
const config1 = {
api: 'https://api.example.com',
port: 8080
} as Record<string, string | number>;
// config1.api 类型是 string | number

// 使用 satisfies:保持精确类型
const config2 = {
api: 'https://api.example.com',
port: 8080
} satisfies Record<string, string | number>;
// config2.api 类型是 string
// config2.port 类型是 number

// 与 as const 结合
const routes = {
home: '/',
about: '/about',
contact: '/contact'
} as const satisfies Record<string, string>;

// routes.home 类型是 "/",而不是 string

satisfies vs as

特性assatisfies
类型覆盖覆盖推断类型保持推断类型
类型检查在断言点检查确保满足约束
精确类型丢失保留
使用场景需要"欺骗"编译器需要验证同时保持精确类型

常见面试问题

Q1: 类型断言和类型转换的区别?

答案

// 类型断言:只影响编译时,不改变运行时行为
const value: unknown = '123';
const num = value as number; // 编译时认为是 number
console.log(typeof num); // 运行时仍是 'string'

// 类型转换:改变运行时的值
const converted = Number(value); // 真正转换为数字
console.log(typeof converted); // 'number'

// 对比
const str = '123';
const asNum = str as unknown as number; // 类型断言,str 还是字符串
const realNum = parseInt(str, 10); // 类型转换,realNum 是数字

Q2: 何时使用类型断言?

答案

适合使用的场景:

// 1. DOM 操作
const input = document.querySelector('input') as HTMLInputElement;

// 2. 处理 unknown 类型(在检查后)
function processData(data: unknown) {
if (typeof data === 'object' && data !== null) {
const obj = data as Record<string, unknown>;
// 使用 obj
}
}

// 3. 第三方库类型不完善
declare const lib: any;
const result = lib.someMethod() as MyExpectedType;

// 4. 为联合类型收窄
type Status = 'loading' | 'success' | 'error';
const status: Status = 'loading';
if (status === 'loading') {
// status 已经收窄,无需断言
}

应避免使用的场景:

// ❌ 不检查就断言
const data: unknown = getExternalData();
const user = data as User; // 危险!

// ✅ 应该先检查
if (isUser(data)) {
const user = data; // 自动收窄为 User
}

Q3: as const 有什么实际用途?

答案

// 1. 创建字面量联合类型
const STATUS = ['pending', 'active', 'inactive'] as const;
type Status = typeof STATUS[number]; // 'pending' | 'active' | 'inactive'

// 2. 创建类型安全的配置对象
const config = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
},
features: {
darkMode: true,
notifications: false
}
} as const;

// 3. 元组类型
function useMediaQuery(query: string): readonly [boolean, () => void] {
const matches = true;
const toggle = () => {};
return [matches, toggle] as const; // 返回元组而不是数组
}

// 4. 映射对象
const EventTypes = {
CLICK: 'click',
FOCUS: 'focus',
BLUR: 'blur'
} as const;

type EventType = typeof EventTypes[keyof typeof EventTypes];
// 'click' | 'focus' | 'blur'

Q4: 如何安全地处理 JSON.parse?

答案

// 不安全
function parseJson<T>(json: string): T {
return JSON.parse(json) as T; // 危险!
}

// 安全方案 1:运行时校验
function parseJsonSafe<T>(
json: string,
validator: (data: unknown) => data is T
): T | null {
try {
const data = JSON.parse(json);
return validator(data) ? data : null;
} catch {
return null;
}
}

// 使用
interface User {
name: string;
age: number;
}

function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
'age' in data
);
}

const user = parseJsonSafe<User>('{"name":"Alice","age":25}', isUser);

// 安全方案 2:使用 zod
import { z } from 'zod';

const UserSchema = z.object({
name: z.string(),
age: z.number()
});

function parseUserJson(json: string) {
return UserSchema.safeParse(JSON.parse(json));
}

Q5: ! 和 ? 的区别与使用场景?

答案

操作符名称作用
?可选链安全访问可能为 null/undefined 的属性
!非空断言告诉编译器值一定不为 null/undefined
interface User {
profile?: {
avatar?: string;
};
}

const user: User = {};

// ? 可选链:安全,返回 undefined
const avatar1 = user.profile?.avatar; // string | undefined

// ! 非空断言:危险,运行时可能报错
// const avatar2 = user.profile!.avatar!; // 运行时报错

// 最佳实践:优先使用 ? 配合默认值
const avatar3 = user.profile?.avatar ?? 'default.png';

// ! 适用场景:你确定值存在
const element = document.getElementById('app');
if (element) {
element.innerHTML = 'Hello'; // 不需要 !,已经在 if 中收窄
}

// 某些框架场景
class Component {
// 知道框架会注入
@Inject() private service!: MyService;
}

相关链接