fec-typescript-type-safetylisted
Install: claude install-skill bovinphang/frontend-craft
# TypeScript 类型安全
## Purpose
为前端代码建立可演进的类型契约,减少 `any`、断言和运行时形状漂移。
## Procedure
### 1. 先确定类型边界
把类型分为外部输入、领域模型、UI view model、组件 props、工具函数 API。不要让后端 DTO、表单模型和 UI 展示模型互相冒充。
```ts
interface UserDto {
id: string;
display_name: string;
status: "ACTIVE" | "DISABLED";
}
interface UserViewModel {
id: string;
displayName: string;
status: "active" | "disabled";
}
export function mapUserDto(dto: UserDto): UserViewModel {
return {
id: dto.id,
displayName: dto.display_name,
status: dto.status === "ACTIVE" ? "active" : "disabled",
};
}
```
### 2. 用 `unknown` 和收窄处理不可信数据
外部输入先校验再使用。不要用 `as` 让编译器闭嘴。
```ts
interface ApiErrorBody {
message: string;
}
function isApiErrorBody(value: unknown): value is ApiErrorBody {
return (
typeof value === "object" &&
value !== null &&
"message" in value &&
typeof value.message === "string"
);
}
export function getApiErrorMessage(value: unknown): string {
return isApiErrorBody(value) ? value.message : "Unexpected error";
}
```
### 3. 用判别联合表达状态机
异步状态、权限分支、支付状态等有限状态,用判别字段让新增分支在编译期暴露。
```ts
type Loadable<T> =
| { state: "idle" }
| { state: "loading" }
| { state: "success"; data: T }
| { state: "error"; error: Error };
function assertNever(value: never): never {
throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}
export function renderUserState(user: Loadable<{ name: string }>): string {
switch (user.state) {
case "idle":
return "Ready";
case "loading":