08. TypeScript进阶:接口、泛型与类型体操

TypeScript 进阶:接口、泛型与类型体操

TypeScript Advanced

上一期我们学习了 TypeScript 的基础类型。这一期,我们深入接口、泛型和高级类型系统——这些是 TypeScript 真正强大的地方。

接口(Interface)

接口定义了对象的形状。

基本用法

interface User {
  id: number
  name: string
  email: string
}

function printUser(user: User) {
  console.log(`${user.name} `)
}

printUser({
  id: 1,
  name: "Alice",
  email: "alice@example.com"
})

可选属性

interface User {
  id: number
  name: string
  email?: string  // 可选
}

let user1: User = { id: 1, name: "Alice" }
let user2: User = { id: 2, name: "Bob", email: "bob@example.com" }

只读属性

interface User {
  readonly id: number
  name: string
}

let user: User = { id: 1, name: "Alice" }
user.id = 2  // Error: Cannot assign to 'id' because it is read-only

函数类型

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc = (source, sub) => {
  return source.search(sub) !== -1
}

可索引类型

interface StringArray {
  [index: number]: string
}

let myArray: StringArray = ["Bob", "Fred"]
let myStr: string = myArray[0]

类类型

interface ClockInterface {
  currentTime: Date
  setTime(d: Date): void
}

class Clock implements ClockInterface {
  currentTime: Date = new Date()
  
  setTime(d: Date) {
    this.currentTime = d
  }
  
  constructor(h: number, m: number) {}
}

继承接口

interface Shape {
  color: string
}

interface Square extends Shape {
  sideLength: number
}

let square: Square = {
  color: "blue",
  sideLength: 10
}

多继承:

interface Shape {
  color: string
}

interface PenStroke {
  penWidth: number
}

interface Square extends Shape, PenStroke {
  sideLength: number
}

泛型(Generics)

泛型让我们可以编写可重用的组件。

基本泛型

function identity(arg: T): T {
  return arg
}

let output1 = identity("myString")  // 类型为 string
let output2 = identity(123)  // 类型推断为 number

泛型变量

function loggingIdentity(arg: T[]): T[] {
  console.log(arg.length)
  return arg
}

泛型类型

let myIdentity: (arg: T) => T = identity

// 对象字面量形式
let myIdentity2: { (arg: T): T } = identity

泛型接口

interface GenericIdentityFn {
  (arg: T): T
}

function identity(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity

把泛型参数移到接口:

interface GenericIdentityFn {
  (arg: T): T
}

function identity(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity

泛型类

class GenericNumber {
  zeroValue: T
  add: (x: T, y: T) => T
}

let myGenericNumber = new GenericNumber()
myGenericNumber.zeroValue = 0
myGenericNumber.add = (x, y) => x + y

泛型约束

interface Lengthwise {
  length: number
}

function loggingIdentity(arg: T): T {
  console.log(arg.length)  // 现在可以访问 .length
  return arg
}

loggingIdentity("hello")  // OK
loggingIdentity([1, 2, 3])  // OK
loggingIdentity(123)  // Error

在泛型中使用类类型

function create(c: { new(): T }): T {
  return new c()
}

class BeeKeeper {
  hasMask: boolean = true
}

class ZooKeeper {
  nametag: string = "Mikle"
}

class Animal {
  numLegs: number = 4
}

class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper()
}

class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper()
}

function createInstance<A>(c: new () => A): A {
  return new c()
}

createInstance(Lion).keeper.nametag
createInstance(Bee).keeper.hasMask

高级类型

交叉类型

将多个类型合并为一个。

interface Person {
  name: string
}

interface Loggable {
  log(): void
}

type PersonLoggable = Person & Loggable

const person: PersonLoggable = {
  name: "Alice",
  log() {
    console.log(this.name)
  }
}

联合类型

type StringOrNumber = string | number

function add(a: StringOrNumber, b: StringOrNumber) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString()
  }
  return a + b
}

类型保护

interface Fish {
  swim(): void
}

interface Bird {
  fly(): void
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim()
  } else {
    pet.fly()
  }
}

typeof 类型保护

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value
  }
  if (typeof padding === "string") {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

instanceof 类型保护

class Dog {
  bark() {}
}

class Cat {
  meow() {}
}

function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark()
  } else {
    pet.meow()
  }
}

null 检查

function f(str: string | null) {
  if (str === null) {
    return "default"
  }
  return str.toUpperCase()
}

可选链:

let x = foo?.bar.baz()
// 等价于
let x = foo === null || foo === undefined ? undefined : foo.bar.baz()

空值合并:

let x = foo ?? "default"
// 等价于
let x = foo !== null && foo !== undefined ? foo : "default"

类型推断

基础推断

let x = 3  // 推断为 number
let y = [0, 1, null]  // 推断为 (number | null)[]

上下文推断

window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.button)  // 推断为 MouseEvent
}

类型推断方向

// 从右向左
let x = 3

// 从左向右(上下文类型)
window.onmousedown = function(mouseEvent) {
  return mouseEvent.button
}

映射类型

从旧类型创建新类型。

Partial

所有属性变为可选。

type Partial = {
  [P in keyof T]?: T[P]
}

interface User {
  id: number
  name: string
}

type PartialUser = Partial
// { id?: number; name?: string; }

Required

所有属性变为必选。

type Required = {
  [P in keyof T]-?: T[P]
}

Readonly

所有属性变为只读。

type Readonly = {
  readonly [P in keyof T]: T[P]
}

Pick

选取部分属性。

type Pick = {
  [P in K]: T[P]
}

interface User {
  id: number
  name: string
  email: string
}

type UserPreview = Pick
// { id: number; name: string; }

Omit

排除部分属性。

type Omit = Pick<T, Exclude>

type UserWithoutEmail = Omit
// { id: number; name: string; }

Record

创建对象类型。

type Record = {
  [P in K]: T
}

type UserMap = Record

条件类型

根据条件选择类型。

type ReturnType = T extends (...args: any[]) => infer R ? R : any

function foo(): number {
  return 1
}

type R = ReturnType  // number

实战示例

API 响应类型

interface ApiResponse {
  code: number
  message: string
  data: T
}

interface User {
  id: number
  name: string
}

async function fetchUser(id: number): Promise<ApiResponse> {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

// 使用
const result = await fetchUser(1)
console.log(result.data.name)

事件处理

interface EventHandler {
  (event: T): void
}

interface ClickEvent {
  type: "click"
  x: number
  y: number
}

const handler: EventHandler = (event) => {
  console.log(`Clicked at ${event.x}, ${event.y}`)
}

状态管理

type State = {
  user: User | null
  loading: boolean
  error: string | null
}

type Action =
  | { type: "FETCH_START" }
  | { type: "FETCH_SUCCESS"; payload: User }
  | { type: "FETCH_ERROR"; payload: string }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "FETCH_START":
      return { ...state, loading: true }
    case "FETCH_SUCCESS":
      return { ...state, loading: false, user: action.payload }
    case "FETCH_ERROR":
      return { ...state, loading: false, error: action.payload }
  }
}

总结

TypeScript 的高级特性:

  • 接口:定义对象形状
  • 泛型:创建可重用组件
  • 类型保护:缩小类型范围
  • 映射类型:从旧类型创建新类型
  • 条件类型:根据条件选择类型

下一期,我们学习 TypeScript 的项目配置和最佳实践。


进阶资源

Views: 24

10. Vue3 + TypeScript:完整集成指南

Vue3 + TypeScript:完整集成指南

Vue3 TypeScript

恭喜你走到最后一期!前面我们学习了 ES6、Vite、Vue3 基础、组合式 API 和 TypeScript。现在,让我们把 TypeScript 完整集成到 Vue3 项目中。

项目创建

Vite + Vue + TypeScript

npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install

项目结构

src/
├── main.ts
├── App.vue
├── vite-env.d.ts
├── components/
│   └── HelloWorld.vue
├── views/
├── router/
│   └── index.ts
├── stores/
│   └── user.ts
├── types/
│   └── index.ts
├── api/
│   └── user.ts
├── composables/
│   └── useUser.ts
└── utils/
    └── format.ts

类型定义

全局类型

// types/index.ts
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
}

export interface ApiResponse {
  code: number
  message: string
  data: T
}

export type Theme = 'light' | 'dark'

export interface AppState {
  user: User | null
  theme: Theme
  loading: boolean
}

组件 Props 类型


interface Props {
  title: string
  count?: number
  users: User[]
}

const props = withDefaults(defineProps(), {
  count: 0
})

组件 Emits 类型


interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'change', event: Event): void
}

const emit = defineEmits()

function handleUpdate() {
  emit('update', 'new value')
}

组合式 API + TypeScript

ref

import { ref } from 'vue'

// 类型推断
const count = ref(0)
const name = ref('Alice')

// 显式类型
const user = ref(null)
const items = ref([])

reactive

import { reactive } from 'vue'

interface State {
  count: number
  name: string
  items: string[]
}

const state = reactive({
  count: 0,
  name: '',
  items: []
})

computed

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 自动推断返回类型
const fullName = computed(() => <code>${firstName.value} ${lastName.value})

// 显式类型
const userCount = computed(() => users.value.length)

组合式函数

// composables/useUser.ts
import { ref, computed } from 'vue'
import type { User } from '@/types'

export function useUser() {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const isLoggedIn = computed(() => !!user.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  async function fetchUser(id: number) {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(<code>/api/users/${id})
      user.value = await response.json()
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error'
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
  }

  return {
    user,
    loading,
    error,
    isLoggedIn,
    userName,
    fetchUser,
    logout
  }
}

Pinia + TypeScript

Store 类型定义

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })

    const data = await response.json()
    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    user,
    token,
    isLoggedIn,
    userName,
    login,
    logout
  }
}, {
  persist: true
})

使用 Store


import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构(保持响应性)
const { user, isLoggedIn, userName } = storeToRefs(userStore)

// Actions
const { login, logout } = userStore

// 表单
const email = ref('')
const password = ref('')

async function handleLogin() {
  await login(email.value, password.value)
}

  <div>
    <p>Welcome, {{ userName }}</p>
    <button>Logout</button>
  </div>

    <button type="submit">Login</button>

Vue Router + TypeScript

路由类型

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/users/:id',
    name: 'User',
    component: () => import('@/views/User.vue'),
    props: true,
    meta: {
      requiresAuth: true
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

路由元信息类型

// types/router.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
    roles?: string[]
  }
}

使用路由


import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// 获取参数(类型安全)
const userId = computed(() => Number(route.params.id))

// 编程式导航
function goToUser(id: number) {
  router.push({ name: 'User', params: { id } })
}

API 集成

类型安全的 API 客户端

// api/client.ts
import type { ApiResponse } from '@/types'

class ApiClient {
  private baseUrl: string

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
  }

  private async request(
    path: string,
    options?: RequestInit
  ): Promise {
    const response = await fetch(<code>${this.baseUrl}${path}, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers
      }
    })

    if (!response.ok) {
      throw new Error(HTTP error! status: ${response.status})
    }

    return response.json()
  }

  get(path: string): Promise {
    return this.request(path)
  }

  post(path: string, data: unknown): Promise {
    return this.request(path, {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }

  put(path: string, data: unknown): Promise {
    return this.request(path, {
      method: 'PUT',
      body: JSON.stringify(data)
    })
  }

  delete(path: string): Promise {
    return this.request(path, {
      method: 'DELETE'
    })
  }
}

export const api = new ApiClient('/api')

API 模块

// api/user.ts
import { api } from './client'
import type { User, ApiResponse } from '@/types'

export const userApi = {
  getAll(): Promise {
    return api.get<ApiResponse>('/users').then(r => r.data)
  },

  getById(id: number): Promise {
    return api.get<ApiResponse>(<code>/users/${id}).then(r => r.data)
  },

  create(data: Omit): Promise {
    return api.post<ApiResponse>('/users', data).then(r => r.data)
  },

  update(id: number, data: Partial): Promise {
    return api.put<ApiResponse>(/users/${id}, data).then(r => r.data)
  },

  delete(id: number): Promise {
    return api.delete(/users/${id})
  }
}

使用 API


import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import type { User } from '@/types'

const users = ref([])
const loading = ref(false)
const error = ref(null)

async function fetchUsers() {
  loading.value = true
  error.value = null

  try {
    users.value = await userApi.getAll()
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Unknown error'
  } finally {
    loading.value = false
  }
}

onMounted(fetchUsers)

  <div>Loading...</div>
  <div>{{ error }}</div>
  <ul>
    <li>
      {{ user.name }}
    </li>
  </ul>

表单处理

类型安全的表单


import { ref, reactive } from 'vue'

interface FormData {
  name: string
  email: string
  age: number
  subscribe: boolean
}

interface FormErrors {
  name?: string
  email?: string
  age?: string
}

const form = reactive({
  name: '',
  email: '',
  age: 0,
  subscribe: false
})

const errors = reactive({})

function validate(): boolean {
  let isValid = true

  if (!form.name.trim()) {
    errors.name = 'Name is required'
    isValid = false
  }

  if (!form.email.includes('@')) {
    errors.email = 'Invalid email'
    isValid = false
  }

  if (form.age  150) {
    errors.age = 'Invalid age'
    isValid = false
  }

  return isValid
}

async function handleSubmit() {
  if (!validate()) return

  // 提交表单
  await submitForm(form)
}

    <div>

      <span>{{ errors.name }}</span>
    </div>

    <div>

      <span>{{ errors.email }}</span>
    </div>

    <div>

      <span>{{ errors.age }}</span>
    </div>

    <div>
      <label>

        Subscribe to newsletter
      </label>
    </div>

    <button type="submit">Submit</button>

使用 vee-validate

npm install vee-validate zod @vee-validate/zod

import { useForm, useField } from 'vee-validate'
import { z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'

const schema = toTypedSchema(
  z.object({
    name: z.string().min(1, 'Name is required'),
    email: z.string().email('Invalid email'),
    age: z.number().min(0).max(150)
  })
)

const { handleSubmit, errors } = useForm({
  validationSchema: schema
})

const { value: name } = useField('name')
const { value: email } = useField('email')
const { value: age } = useField('age')

const onSubmit = handleSubmit((values) => {
  console.log(values)
})

工具类型

组件实例类型

import type { ComponentInstance } from 'vue'

// 获取组件实例类型
type MyComponentInstance = ComponentInstance

提取 Props 类型

import type { ExtractPropTypes } from 'vue'

const propsDefinition = {
  title: String,
  count: {
    type: Number,
    default: 0
  }
} as const

type Props = ExtractPropTypes
// { title: string; count: number }

组件自定义类型

// 自定义组件类型
declare module '@vue/runtime-core' {
  export interface GlobalComponents {
    MyButton: typeof import('@/components/MyButton.vue')['default']
    MyInput: typeof import('@/components/MyInput.vue')['default']
  }
}

测试

Vitest 配置

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true
  }
})

组件测试

// components/__tests__/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../Button.vue'

describe('Button', () => {
  it('renders with text', () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })
    expect(wrapper.text()).toBe('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

最佳实践

1. 使用 script setup


// 推荐

2. 类型优先

// 先定义类型,再实现
interface User {
  id: number
  name: string
}

const user = ref(null)

3. 使用组合式函数

// 提取可复用逻辑
export function useUser() {
  // ...
}

4. 严格模式

// tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

5. 类型安全的 Props


interface Props {
  title: string
  count?: number
}

const props = withDefaults(defineProps(), {
  count: 0
})

系列总结

恭喜你完成了从 ES6 到 Vue3 + TypeScript 的完整学习之旅!

第一期:TypeScript 基础类型系统
第二期:接口、泛型与类型体操
第三期:项目配置与最佳实践
第四期:TypeScript 与前端框架
第五期:Vue3 + TypeScript 完整集成

你现在掌握了:

  • TypeScript 类型系统
  • Vue3 组合式 API
  • 类型安全的组件开发
  • Pinia 状态管理
  • Vue Router 类型安全
  • API 集成最佳实践

下一步

  • 学习 Nuxt 3(Vue3 全栈框架)
  • 探索 Vite 插件开发
  • 学习服务端渲染
  • 阅读官方文档

进阶资源


恭喜!你已经掌握了 Vue3 + TypeScript 的核心技能。去构建你的下一个项目吧!

Views: 10

07. TypeScript实战:项目配置与最佳实践

TypeScript 实战:项目配置与最佳实践

TypeScript Config

学会了 TypeScript 的类型系统,现在我们来看看如何在实际项目中配置和使用 TypeScript。这一期,我们学习 tsconfig.json、模块系统、声明文件和工程化实践。

tsconfig.json 详解

基本结构

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

常用配置项

目标和模块

{
  "compilerOptions": {
    // 编译目标 ES 版本
    "target": "ES2020",

    // 模块系统
    "module": "ESNext",

    // 模块解析策略
    "moduleResolution": "node",

    // 生成 source map
    "sourceMap": true
  }
}

严格模式

{
  "compilerOptions": {
    // 启用所有严格选项
    "strict": true,

    // 允许从没有设置默认导出的模块中导入
    "allowSyntheticDefaultImports": true,

    // 启用 ES 模块互操作性
    "esModuleInterop": true,

    // 不允许隐式 any
    "noImplicitAny": true,

    // 严格的 null 检查
    "strictNullChecks": true
  }
}

路径映射

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

使用:

import { Button } from '@components/Button'
import { formatDate } from '@utils/date'

输出配置

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

推荐配置

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "moduleResolution": "node",
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "moduleDetection": "force",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

模块系统

ES 模块

// utils.ts
export function add(a: number, b: number): number {
  return a + b
}

export const PI = 3.14

export default class Calculator {
  // ...
}
// main.ts
import Calculator, { add, PI } from './utils'

命名导出

// 导出多个
export { add, subtract, multiply }

// 重命名导出
export { add as sum }

// 全部导出
export * from './math'

动态导入

async function loadModule() {
  const { add } = await import('./utils')
  console.log(add(1, 2))
}

声明文件

什么是声明文件

声明文件(.d.ts)描述 JavaScript 代码的类型信息。

// math.d.ts
declare function add(a: number, b: number): number
declare const PI: number

编写声明文件

// jquery.d.ts
declare const $: {
  (selector: string): {
    html(content: string): void
    on(event: string, handler: (e: Event) => void): void
  }
  ajax(options: {
    url: string
    method?: string
    data?: any
    success?: (response: any) => void
  }): void
}

模块声明

// my-module.d.ts
declare module 'my-module' {
  export function doSomething(value: string): number
  export interface Options {
    debug: boolean
  }
  export default class MyClass {
    constructor(options: Options)
    run(): void
  }
}

全局声明

// global.d.ts
declare global {
  interface Window {
    myCustomProperty: string
  }

  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production'
      API_URL: string
    }
  }
}

export {}

DefinitelyTyped

大部分库都有社区维护的声明文件:

npm install --save-dev @types/lodash
npm install --save-dev @types/node
npm install --save-dev @types/react

项目结构

目录组织

src/
├── components/     # 组件
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.styles.ts
│   │   ├── Button.types.ts
│   │   └── index.ts
│   └── index.ts
├── hooks/          # 自定义 Hooks
├── utils/          # 工具函数
├── types/          # 全局类型
│   └── index.d.ts
├── api/            # API 接口
├── constants/      # 常量
└── App.tsx

类型文件组织

// types/user.ts
export interface User {
  id: number
  name: string
  email: string
}

export type UserRole = 'admin' | 'user' | 'guest'

export interface UserWithRole extends User {
  role: UserRole
}
// types/api.ts
export interface ApiResponse {
  code: number
  message: string
  data: T
}

export interface PaginatedResponse {
  items: T[]
  total: number
  page: number
  pageSize: number
}
// types/index.ts
export * from './user'
export * from './api'

工具链

ESLint

npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  rules: {
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/explicit-function-return-type': 'off'
  }
}

Prettier

npm install --save-dev prettier eslint-config-prettier
// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}

Husky + lint-staged

npm install --save-dev husky lint-staged
npx husky install
// package.json
{
  "scripts": {
    "lint": "eslint src --ext .ts,.tsx",
    "type-check": "tsc --noEmit"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}
npx husky add .husky/pre-commit "npx lint-staged"

最佳实践

1. 严格模式

始终启用 strict: true

{
  "compilerOptions": {
    "strict": true
  }
}

2. 避免 any

// Bad
function parse(data: any) {
  return JSON.parse(data)
}

// Good
function parse(data: string): unknown {
  return JSON.parse(data)
}

3. 使用 unknown 代替 any

// Bad
let value: any = getValue()
value.foo()  // 不安全

// Good
let value: unknown = getValue()
if (typeof value === 'object' && value !== null && 'foo' in value) {
  (value as { foo: () => void }).foo()
}

4. 类型优先

// Bad
const user = {
  name: 'Alice',
  age: 30
}

// Good
interface User {
  name: string
  age: number
}

const user: User = {
  name: 'Alice',
  age: 30
}

5. 使用类型推断

// Bad
let x: number = 10
let arr: number[] = [1, 2, 3]

// Good(让 TS 推断)
let x = 10
let arr = [1, 2, 3]

6. 使用 const 断言

// 不使用 const 断言
let x = [1, 2]  // number[]
x = [3, 4]      // OK

// 使用 const 断言
let y = [1, 2] as const  // readonly [1, 2]
y = [3, 4]    // Error

7. 使用满足约束

type Colors = 'red' | 'green' | 'blue'

// Bad
const favoriteColor: Colors = 'red'

// Good(更好的类型推断)
const favoriteColor = 'red' as const satisfies Colors

8. 使用模板字面量类型

type EventName = 'click' | 'scroll' | 'mousemove'
type EventHandler = <code>on${Capitalize}
// "onClick" | "onScroll" | "onMousemove"

function addHandler(event: EventHandler, handler: () => void) {
  // ...
}

addHandler('onClick', () => {})  // OK
addHandler('click', () => {})    // Error

调试技巧

使用 tsc --noEmit

只做类型检查,不生成文件:

npx tsc --noEmit

使用 tsc --watch

监视模式:

npx tsc --watch

类型可视化

// 使用工具类型查看推断结果
type ShowType = { [K in keyof T]: T[K] }

type Result = ShowType

常见问题

模块导入问题

// 如果遇到 "Cannot find module" 错误
// 检查 tsconfig.json

{
  "compilerOptions": {
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

类型扩展

// 扩展第三方类型
declare module 'third-party-lib' {
  interface Options {
    customOption: string
  }
}

类型断言滥用

// Bad
const value = something as any as MyType

// Good
function isMyType(value: unknown): value is MyType {
  return typeof value === 'object' && 
         value !== null && 
         'requiredField' in value
}

if (isMyType(something)) {
  // something 是 MyType
}

总结

TypeScript 项目配置要点:

  • tsconfig.json:正确配置编译选项
  • 模块系统:使用 ES 模块
  • 声明文件:为 JS 库添加类型
  • 工具链:ESLint + Prettier + Husky
  • 最佳实践:严格模式、避免 any、类型优先

下一期,我们学习 TypeScript 与前端框架的结合。


工具推荐

  • ts-node - 直接运行 TypeScript
  • ts-loader - Webpack TypeScript loader
  • tsdx - TypeScript 库开发工具

Views: 18

09. TypeScript与前端框架:React、Node.js

TypeScript 与前端框架:React、Node.js

TypeScript Frameworks

TypeScript 与现代前端框架是天生一对。这一期,我们学习 TypeScript 在 React 和 Node.js 中的应用。

React + TypeScript

创建项目

# Vite(推荐)
npm create vite@latest my-app -- --template react-ts

# Create React App
npx create-react-app my-app --template typescript

# Next.js
npx create-next-app@latest my-app --typescript

组件类型

函数组件

// 简单组件
const Button: React.FC = () => {
  return <button>Click me</button>
}

// 带 Props 的组件
interface ButtonProps {
  text: string
  onClick: () => void
  disabled?: boolean
}

const Button: React.FC = ({ 
  text, 
  onClick, 
  disabled = false 
}) => {
  return (
    <button disabled="{disabled}">
      {text}
    </button>
  )
}

内联 Props

type UserCardProps = {
  name: string
  email: string
  avatar?: string
}

function UserCard({ name, email, avatar }: UserCardProps) {
  return (
    <div>
      {avatar && <img src="{avatar}" alt="{name}" />}
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

Hooks 类型

useState

// 类型推断
const [count, setCount] = useState(0)

// 显式类型
const [user, setUser] = useState(null)

// 复杂类型
const [items, setItems] = useState([])

useEffect

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数
  }
}, [dependency])

useRef

// DOM 引用
const inputRef = useRef(null)

// 任意值引用
const timerRef = useRef(null)

useContext

interface ThemeContextType {
  theme: 'light' | 'dark'
  toggleTheme: () => void
}

const ThemeContext = createContext(null)

function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}

useReducer

type State = {
  count: number
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return { count: action.payload }
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 })

事件类型

function Form() {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
  }

  const handleChange = (e: React.ChangeEvent) => {
    console.log(e.target.value)
  }

  const handleClick = (e: React.MouseEvent) => {
    console.log('clicked')
  }

  return (

      <button>Submit</button>

  )
}

泛型组件

interface ListProps {
  items: T[]
  renderItem: (item: T) => React.ReactNode
}

function List({ items, renderItem }: ListProps) {
  return (
    <ul>
      {items.map((item, index) => (
        <li>{renderItem(item)}</li>
      ))}
    </ul>
  )
}

// 使用
 <span>{user.name}</span>}
/>

高阶组件

interface WithLoadingProps {
  loading?: boolean
}

function withLoading<P>(
  Component: React.ComponentType<P>
): React.FC<P> {
  return ({ loading, ...props }) => {
    if (loading) {
      return <div>Loading...</div>
    }
    return 
  }
}

Node.js + TypeScript

项目设置

mkdir my-project
cd my-project
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Express 示例

npm install express
npm install @types/express --save-dev
import express, { Request, Response, NextFunction } from 'express'

const app = express()
app.use(express.json())

// 类型定义
interface User {
  id: number
  name: string
  email: string
}

// 内存数据库
const users: User[] = []

// 路由
app.get('/users', (req: Request, res: Response) => {
  res.json(users)
})

app.get('/users/:id', (req: Request, res: Response) => {
  const user = users.find(u => u.id === parseInt(req.params.id))
  if (!user) {
    res.status(404).send('User not found')
    return
  }
  res.json(user)
})

app.post('/users', (req: Request<{}, User, Omit>, res: Response) => {
  const newUser: User = {
    id: users.length + 1,
    ...req.body
  }
  users.push(newUser)
  res.status(201).json(newUser)
})

// 错误处理
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack)
  res.status(500).json({ error: err.message })
})

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000')
})

中间件类型

interface AuthRequest extends Request {
  user?: {
    id: number
    role: string
  }
}

function authMiddleware(
  req: AuthRequest,
  res: Response,
  next: NextFunction
) {
  const token = req.headers.authorization
  if (!token) {
    res.status(401).json({ error: 'Unauthorized' })
    return
  }

  // 验证 token...
  req.user = { id: 1, role: 'admin' }
  next()
}

app.get('/protected', authMiddleware, (req: AuthRequest, res: Response) => {
  res.json({ user: req.user })
})

数据库集成

// Prisma 示例
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function getUsers() {
  const users = await prisma.user.findMany({
    include: { posts: true }
  })
  return users
}

async function createUser(data: { name: string; email: string }) {
  const user = await prisma.user.create({
    data
  })
  return user
}

工具函数

API 客户端

interface HttpClient {
  get(url: string): Promise
  post(url: string, data: unknown): Promise
  put(url: string, data: unknown): Promise
  delete(url: string): Promise
}

class FetchHttpClient implements HttpClient {
  private baseUrl: string

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
  }

  async get(url: string): Promise {
    const response = await fetch(<code>${this.baseUrl}${url})
    if (!response.ok) {
      throw new Error(HTTP error! status: ${response.status})
    }
    return response.json()
  }

  async post(url: string, data: unknown): Promise {
    const response = await fetch(${this.baseUrl}${url}, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    return response.json()
  }

  // ... 其他方法
}

状态管理

// 简单的状态管理器
type Listener = (state: T) => void

class Store {
  private state: T
  private listeners: Listener[] = []

  constructor(initialState: T) {
    this.state = initialState
  }

  getState(): T {
    return this.state
  }

  setState(newState: Partial) {
    this.state = { ...this.state, ...newState }
    this.listeners.forEach(listener => listener(this.state))
  }

  subscribe(listener: Listener) {
    this.listeners.push(listener)
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener)
    }
  }
}

// 使用
interface AppState {
  user: User | null
  loading: boolean
}

const store = new Store({
  user: null,
  loading: false
})

测试

Jest 配置

npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init

单元测试

// math.ts
export function add(a: number, b: number): number {
  return a + b
}

// math.test.ts
import { add } from './math'

describe('add', () => {
  it('should add two numbers', () => {
    expect(add(1, 2)).toBe(3)
  })
})

React 组件测试

// Button.tsx
interface ButtonProps {
  text: string
  onClick: () => void
}

export const Button: React.FC = ({ text, onClick }) => {
  return <button>{text}</button>
}

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'

describe('Button', () => {
  it('should render with text', () => {
    render(<Button> {}} />)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('should call onClick when clicked', () => {
    const handleClick = jest.fn()
    render(<Button />)
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

实战技巧

类型安全的 API 路由

// 路由定义
type Route = {
  path: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  handler: (req: Request, res: Response) => void | Promise
}

const routes: Route[] = [
  {
    path: '/users',
    method: 'GET',
    handler: getUsers
  },
  {
    path: '/users',
    method: 'POST',
    handler: createUser
  }
]

// 自动注册路由
routes.forEach(route => {
  app[route.method.toLowerCase()](route.path, route.handler)
})

类型安全的环境变量

// env.ts
import { z } from 'zod'

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.string().transform(Number),
  DATABASE_URL: z.string(),
  JWT_SECRET: z.string()
})

export const env = envSchema.parse(process.env)

总结

TypeScript 与框架结合要点:

  • React:组件类型、Hooks 类型、事件类型
  • Node.js:Express 类型、中间件、数据库
  • 测试:Jest + ts-jest
  • 最佳实践:类型安全的 API、环境变量

下一期,我们学习 Vue3 + TypeScript 的完整集成指南。


框架文档

Views: 5

06. TypeScript基础:类型系统入门

TypeScript 基础:类型系统入门

TypeScript

JavaScript 是动态类型语言,变量类型在运行时才确定。这带来了灵活性,但也导致了很多运行时错误。TypeScript 给 JavaScript 加上了静态类型系统,让错误在编译时就能被发现。

这一期,我们学习 TypeScript 的基础类型系统。

为什么需要 TypeScript

JavaScript 的痛点

// 这段代码在 JavaScript 中完全合法
let name = "Alice"
name = 123  // 没有报错
name.toUpperCase()  // 运行时报错:name.toUpperCase is not a function

TypeScript 的解决方案

let name: string = "Alice"
name = 123  // 编译时报错:Type 'number' is not assignable to type 'string'

TypeScript 的优势

优势 说明
类型安全 编译时发现错误
智能提示 IDE 自动补全
代码文档 类型即文档
重构友好 改代码有信心

安装 TypeScript

全局安装

npm install -g typescript

项目安装

npm install typescript --save-dev

初始化配置

npx tsc --init

这会生成 tsconfig.json 文件。

基础类型

布尔值

let isDone: boolean = false

数字

let decimal: number = 6
let hex: number = 0xf00d
let binary: number = 0b1010
let octal: number = 0o744

字符串

let color: string = "blue"
color = 'red'
color = <code>green

数组

// 方式1:元素类型后面接 []
let list1: number[] = [1, 2, 3]

// 方式2:使用数组泛型
let list2: Array = [1, 2, 3]

元组

元组表示已知元素数量和类型的数组。

let tuple: [string, number]

tuple = ["hello", 10]  // OK
tuple = [10, "hello"]  // Error

访问元素:

console.log(tuple[0].substring(1))  // OK
console.log(tuple[1].substring(1))  // Error, number 没有 substring

枚举

enum Color {
  Red,
  Green,
  Blue
}

let c: Color = Color.Green
console.log(c)  // 1

自定义值:

enum Color {
  Red = 1,
  Green = 2,
  Blue = 4
}

let c: Color = Color.Green

反向映射:

enum Color {
  Red = 1,
  Green,
  Blue
}

let colorName: string = Color[2]
console.log(colorName)  // 'Green'

any

任意类型,放弃类型检查。

let notSure: any = 4
notSure = "maybe a string"
notSure = false

注意:尽量少用 any,否则就失去了 TypeScript 的意义。

unknown

类型安全的 any。

let notSure: unknown = 4

// 必须类型检查后才能使用
if (typeof notSure === "number") {
  console.log(notSure.toFixed(2))
}

void

没有任何类型,通常用于函数返回值。

function warnUser(): void {
  console.log("This is my warning message")
}

null 和 undefined

let u: undefined = undefined
let n: null = null

默认情况下 nullundefined 是所有类型的子类型。

never

永不存在的值的类型。

// 抛出异常的函数
function error(message: string): never {
  throw new Error(message)
}

// 无限循环
function infiniteLoop(): never {
  while (true) {}
}

object

非原始类型。

declare function create(o: object | null): void

create({ prop: 0 })  // OK
create(null)         // OK
create(42)           // Error
create("string")     // Error

类型断言

告诉 TypeScript "我知道这个类型"。

尖括号语法

let someValue: any = "this is a string"

let strLength: number = (someValue).length

as 语法(推荐)

let someValue: any = "this is a string"

let strLength: number = (someValue as string).length

类型推断

TypeScript 会自动推断类型。

let x = 3  // 推断为 number
let y = "hello"  // 推断为 string

// 从右向左推断
let z = x + 1  // number

联合类型

表示可以是多种类型之一。

let id: string | number

id = "abc123"  // OK
id = 123       // OK
id = false     // Error

实际应用:

function formatValue(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase()
  } else {
    return value.toFixed(2)
  }
}

字面量类型

精确到具体的值。

let x: "hello"
x = "hello"  // OK
x = "world"  // Error

联合字面量:

type Direction = "up" | "down" | "left" | "right"

function move(dir: Direction) {
  // ...
}

move("up")     // OK
move("north")  // Error

类型别名

给类型起个名字。

type Name = string
type NameResolver = () => string
type NameOrResolver = Name | NameResolver

function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n
  } else {
    return n()
  }
}

实践建议

1. 避免使用 any

// Bad
let data: any = fetchData()

// Good
interface User {
  id: number
  name: string
}

let data: User = fetchData()

2. 使用类型推断

// Bad
let x: number = 10

// Good(让 TS 推断)
let x = 10

3. 优先使用 interface

// Bad
type User = {
  name: string
}

// Good(interface 可扩展)
interface User {
  name: string
}

4. 使用严格模式

tsconfig.json 中启用:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

总结

TypeScript 的基础类型系统包括:

  • 原始类型:boolean, number, string
  • 复合类型:array, tuple, enum
  • 特殊类型:any, unknown, void, never
  • 类型操作:联合类型、类型断言、类型推断

掌握了这些,你已经可以用 TypeScript 写代码了。下一期,我们学习接口、泛型和高级类型。


学习资源

Views: 13