TypeScript使用方法(AI 大模型评测平台的类型安全开发)

立项第一天定的规矩只有一条:全量 TypeScript。定这个规矩是因为吃过纯 JS 的亏:上一个项目跑到四万行的时候,改一个接口字段名,得全局搜索所有调用处,漏改一处就是线上白屏。评测平台这种前后端接口特别多的系统,类型就是前置的一层测试。下面分享平台对 TS 的全套实践:类型系统、工具、避坑。

一、为什么必须用 TypeScript

JS 项目到 5 万行以上时,重构一个函数要靠”全局搜索同名调用方”,效率断崖式下降。TS 提供的类型系统让 IDE 能在改一处时自动标红所有受影响的地方。平台核心收益:

  1. 重构安全:函数签名改了,所有调用方立即在 IDE 标红;
  2. IDE 智能提示:编辑器能自动补全、自动推断属性;
  3. 编译期错误:常见 typo、未传必填参数、类型不匹配都在编译时发现;
  4. 文档作用:类型签名就是 API 文档,新人接手快。

四项收益里,重构安全的性价比很高。评测平台有个”模型版本升级”的需求,涉及十来个接口的返回结构调整,全量 TS 让团队在半天内完成迁移,编译期把所有受影响处揪了出来,一次上线没出问题。这笔账算下来,TS 的初始成本早就赚回来了。

二、tsconfig 关键配置

{
    "compilerOptions": {
        "target": "ES2022",
        "module": "ESNext",
        "moduleResolution": "Bundler",
        "strict": true,                // 严格模式
        "noUncheckedIndexedAccess": true,  // arr[i] 类型是 T | undefined
        "noImplicitAny": true,
        "strictNullChecks": true,      // null/undefined 严格区分
        "esModuleInterop": true,
        "skipLibCheck": true,
        "isolatedModules": true,
        "resolveJsonModule": true,
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        },
        "types": ["vite/client", "element-plus/global"]
    },
    "include": ["src/**/*", "src/**/*.vue"]
}

strict: true 是关键,平台从 Day 1 就开。它相当于一次性打开 strictNullChecks、noImplicitAny 等多项严格检查,把大量运行时错误提前到编译期。我的建议是宁可一开始多改几处报错,也不要先关后开——项目大了以后再开 strict,改动面会大到无法收场。noUncheckedIndexedAccess 是 strict 之外我们额外加的,它让数组取值变成 T | undefined,逼着你判空,评测报告里那个榜单下标越界的问题就是这么被提前拦下的。

三、Vue 3 + TS 集成

1. 组件 Props 类型

// 用 type-only defineProps
const props = defineProps<{
    list: Item[]
    initial?: number
    onChange?: (v: number) => void
}>()

2. 组件 Emits 类型

const emit = defineEmits<{
    (e: 'change', value: number): void
    (e: 'update', item: Item): void
}>()

3. ref 类型

const count = ref<number>(0)
const user = ref<User | null>(null)
const list = ref<Item[]>([])

4. 模板 ref 类型

import { ref } from 'vue'

const inputRef = ref<HTMLInputElement | null>(null)

onMounted(() => {
    inputRef.value?.focus()  // 注意可能 null
})

5. 路由参数类型

import type { RouteLocationNormalized } from 'vue-router'

const route = useRoute()
const sessionId = computed(() => {
    const id = route.params.id
    return typeof id === 'string' ? parseInt(id, 10) : 0
})

这几段是 Vue 3 里比较常见的类型写法。模板 ref 那里容易翻车——ref<HTMLInputElement | null>(null) 的 null 必须写,否则类型上认为它永远有值,后面用 ?. 是安全的写法。路由参数的值可能是 string 也可能是数组,typeof 收一下再 parseInt,能少很多线上崩溃。

四、API 客户端类型

平台给每个接口定义请求/响应类型:

// types/api.ts
export interface LoginRequest {
    username: string
    password: string
}

export interface LoginResponse {
    token: string
    userId: number
    expiresAt: string
}

export interface EvalSession {
    id: number
    userId: number
    question: string
    status: 'pending' | 'running' | 'done' | 'error'
    items: EvalItem[]
    createdAt: string
}

// api/client.ts
export const api = {
    post<TReq, TRes>(url: string, data: TReq): Promise<TRes> {
        return http.post(url, data).then(r => r.data.data as TRes)
    }
}

// 使用
const res = await api.post<LoginRequest, LoginResponse>(
    '/api/auth/login',
    { username: 'admin', password: 'xxx' }
)
// res 类型自动推导为 LoginResponse

接口类型定义是平台类型体系的核心。评测会话的状态用了联合类型 'pending' | 'running' | 'done' | 'error',switch 的时候写漏一个分支编译期就会提示,比运行时排查省事得多。这块要跟后端对齐——我们前后端共用一份接口契约生成类型,避免手写类型跟实际返回对不上。

五、类型守卫

// 1) typeof 守卫
if (typeof value === 'string') {
    value.toUpperCase()  // 这里是 string
}

// 2) instanceof 守卫
if (error instanceof AxiosError) {
    console.log(error.response?.data)
}

// 3) 自定义守卫
interface ApiError {
    code: number
    message: string
}

function isApiError(e: unknown): e is ApiError {
    return typeof e === 'object' && e !== null 
        && 'code' in e && 'message' in e
}

try {
    await api.post(...)
} catch (e) {
    if (isApiError(e)) {
        ElMessage.error(e.message)
    }
}

类型守卫解决的是”unknown 怎么用”的问题。网络请求的 catch 分支拿到的错误类型是 unknown,直接访问属性会被编译拦下。自定义守卫 isApiError 是平台用得比较多的写法,配合响应拦截器,把”后端返回的业务错误”和”网络错误”区分开处理。

六、工具类型

// 1) Partial<T> - 所有字段可选
type UpdateUser = Partial<User>

// 2) Pick<T, K> - 选字段
type UserBrief = Pick<User, 'id' | 'name' | 'avatar'>

// 3) Omit<T, K> - 去字段
type UserWithoutPassword = Omit<User, 'password'>

// 4) ReturnType<T> - 提取函数返回类型
type ApiResponse = ReturnType<typeof api.getUser>

// 5) Awaited<T> - 解 Promise
type User = Awaited<ReturnType<typeof api.getUser>>

// 6) Record<K, V> - 键值对
type ModelMap = Record<string, ModelInfo>

// 7) Required<T> - 所有字段必填
type StrictConfig = Required<Config>

平台对常用类型用这些工具组合,避免重复定义。这里用的比较多的组合是 ReturnType + Awaited:接口函数的返回类型变了,引用它的类型自动跟着变,不用手动维护第二份。用 Omit 去掉 password 字段这种写法,也避免了”多复制一份 User 类型”带来的漂移问题。

七、泛型组件

// 一个通用 List 组件
interface ListProps<T> {
    items: T[]
    itemKey: keyof T
}

const props = defineProps<ListProps<Model>>()

// 推导 items 元素类型
type Item = typeof props.items[number]

泛型组件让”一个列表组件吃所有模型”成为可能。传进去的 items 类型不同,itemKey 的校验也跟着变。平台把这个用于评测历史、模型列表、用户列表,省掉了三份几乎一样的组件代码。

八、类型与第三方库

// Element Plus 类型增强
import type { ElMessage } from 'element-plus'

// Axios 拦截器类型
import type { AxiosRequestConfig, AxiosResponse } from 'axios'

http.interceptors.response.use(
    (response: AxiosResponse) => response.data.code === 0 
        ? response : Promise.reject(response)
)

// Vue Router 路由 meta 类型扩展
declare module 'vue-router' {
    interface RouteMeta {
        requiresAuth?: boolean
        title?: string
        icon?: string
    }
}

第三方库的类型声明是平台常踩的雷区。Element Plus 和 axios 自带类型还好,Vue Router 的 RouteMeta 需要自己扩展。declare module 的写法要注意:全项目只在一个文件里声明,重复声明会报错。

九、避免 any 的实战技巧

// ❌ 滥用 any
function process(data: any) { return data.foo }

// ✅ unknown 强制类型检查
function process(data: unknown) {
    if (typeof data === 'object' && data !== null && 'foo' in data) {
        return (data as { foo: string }).foo
    }
}

// ✅ 部分字段确定时用类型断言
const data = await fetch(url).then(r => r.json()) as MyType

// ✅ zod 运行时校验
import { z } from 'zod'
const UserSchema = z.object({
    id: z.number(),
    name: z.string()
})
const user = UserSchema.parse(await api.get(...))

any 的克制程度直接决定项目类型安全的成色。我们的规则是:代码评审里出现 any 必须有注释说明理由,否则打回。unknown 加类型守卫是首选,zod 用在”边界校验”——后端返回的数据经过 zod parse,类型和运行时双重保证。

十、strict 模式下的常见 TS 错误

开了 strict 之后,报错数量会先涨一波,但大部分是同一类问题。下面这几个错误类型是我们排查频率排在前面的:

错误 原因 解决
Object is possibly 'undefined' 数组/对象访问可能越界 arr[0]?.foo 或显式判空
Property 'xxx' does not exist 字段名写错 查类型定义
Argument of type 'X' is not assignable to parameter of type 'Y' 类型不匹配 显式转换或改函数签名
Cannot find module 'X' 缺类型声明 npm i -D @types/X
'X' is declared but never used 未使用变量 _ 前缀或删掉
`Type ‘X \ null’ is not assignable to type ‘X’` null 严格区分

前两类占掉大半的报错量。习惯之后会发现它们是类型系统在提醒你”这里有隐藏的分支没处理”,逐个修完,代码质量上了一个台阶。

十一、ESLint + Prettier

// .eslintrc.cjs
{
    "extends": [
        "eslint:recommended",
        "plugin:vue/vue3-recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "rules": {
        "@typescript-eslint/no-explicit-any": "error",
        "@typescript-eslint/no-unused-vars": ["error", 
            { "argsIgnorePattern": "^_" }],
        "@typescript-eslint/explicit-function-return-type": "off"
    }
}

CI 流程跑 pnpm typecheck + pnpm lint,任何错误阻断 merge。这条我们抓得很严——有次手滑把一行未使用变量提交了,被 lint 拦下后养成了习惯。格式交给 Prettier 自动处理,团队成员不需要争论代码风格。

十二、Vue 文件类型增强

// shims.d.ts
declare module '*.vue' {
    import type { DefineComponent } from 'vue'
    const component: DefineComponent<{}, {}, any>
    export default component
}

现在用 vite + vue-tsc 的话,这一步其实可以省略,vite 的客户端类型已经能识别 .vue 模块。老项目迁移过来时保留这份 shims 文件主要是为了兼容历史配置。

十三、与 Pinia 集成

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface UserState {
    userId: number | null
    token: string
    profile: UserProfile | null
}

export const useUserStore = defineStore('user', () => {
    const userId = ref<UserState['userId']>(null)  // 类型化
    const token = ref<UserState['token']>('')
    // ...
})

Pinia 用 setup 风格写 store 时,类型天然跟 Vue 的 ref/computed 对齐,state 里定义的形状直接用 UserState['xxx'] 引用,state 和类型定义不会漂移。

十四、踩过的坑

  • 类型循环依赖:A 用 B 类型,B 用 A 类型。用 import type 或抽公共类型。
  • ref 自动解包陷阱:arr[i] 在模板是值,在 JS 是 Ref<T> | undefined(开 noUncheckedIndexedAccess 后)。
  • 类型扩展冲突:Vue Router 扩展 RouteMeta 多个文件重复 declare module 报错。统一在一个 router.d.ts 里。
  • 类型与运行时不一致:服务端返回 null 但类型是 {}。用 zod 在边界校验。
  • vue-tsc 慢:项目大了类型检查 30s+。CI 用 vue-tsc --noEmit 增量。
  • vue-tsc 与 IDE 不一致:vue-tsc 是 ground truth,IDE 报红以它为准。

这些坑里,”类型与运行时不一致”是比较隐蔽的。前端类型默认服务端返回符合接口定义,但真实接口有时会返回 null 或者多出字段。我们的解法是在接口边界用 zod 做一次运行时校验,类型系统负责编译期,zod 负责运行时,两边各管一段。

十五、类型覆盖率指标

平台设了硬性目标:

  • any 使用:< 1%
  • 公共函数有显式返回类型:100%
  • 关键路径类型覆盖率:> 95%

每月报告 tsc --noEmit 通过率 + // @ts-expect-error 数量。

指标的意义在于把”类型安全”从口号变成可度量。数字会说话:any 的比例持续走低,说明团队在往更严格的类型方向收敛;@ts-expect-error 的数量异常上涨,往往意味着某块类型定义和真实数据结构脱节了,需要及时排查。

常见问题(FAQ)

Q1:必须 strict 模式吗?

建议开。strict 模式发现的 bug 是非 strict 的 3-5 倍。

Q2:第三方库没类型怎么办?

自己写 declare module 'xxx',或加 // @ts-ignore(不要用 any)。

Q3:类型定义文件放哪?

src/types/ 统一管理。组件 props 类型就近放 .vue 文件。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部