Preact与TypeScript #

一、TypeScript 配置 #

1.1 安装 TypeScript #

bash
npm install -D typescript @types/node

1.2 tsconfig.json #

json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}

1.3 Vite 配置 #

typescript
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()],
  resolve: {
    alias: {
      '@': '/src'
    }
  }
});

二、组件类型 #

2.1 函数组件 #

tsx
import { FunctionalComponent } from 'preact';

interface ButtonProps {
  text: string;
  onClick?: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
}

const Button: FunctionalComponent<ButtonProps> = ({
  text,
  onClick,
  disabled = false,
  variant = 'primary'
}) => {
  return (
    <button
      class={`btn btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
    >
      {text}
    </button>
  );
};

export default Button;

2.2 简化写法 #

tsx
interface CardProps {
  title: string;
  children: preact.ComponentChildren;
}

function Card({ title, children }: CardProps) {
  return (
    <div class="card">
      <h2>{title}</h2>
      <div class="card-body">{children}</div>
    </div>
  );
}

2.3 带泛型的组件 #

tsx
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => preact.VNode;
  keyExtractor: (item: T) => string | number;
}

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

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

<List<User>
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

三、Hooks 类型 #

3.1 useState #

tsx
import { useState } from 'preact/hooks';

function Counter() {
  const [count, setCount] = useState<number>(0);
  const [user, setUser] = useState<User | null>(null);
  const [items, setItems] = useState<string[]>([]);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

3.2 useRef #

tsx
import { useRef } from 'preact/hooks';

function Input() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
    </div>
  );
}

3.3 useEffect #

tsx
import { useEffect } from 'preact/hooks';

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data: User = await response.json();
      
      if (!cancelled) {
        setUser(data);
      }
    }

    fetchUser();

    return () => {
      cancelled = true;
    };
  }, [userId]);

  return <div>{user?.name}</div>;
}

3.4 自定义 Hook #

tsx
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        const json: T = await response.json();
        
        if (!cancelled) {
          setData(json);
          setError(null);
        }
      } catch (e) {
        if (!cancelled) {
          setError((e as Error).message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}

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

function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  
  return <div>{user?.name}</div>;
}

四、事件类型 #

4.1 表单事件 #

tsx
function Form() {
  const handleSubmit = (e: Event) => {
    e.preventDefault();
    console.log('Form submitted');
  };

  const handleInput = (e: Event) => {
    const target = e.target as HTMLInputElement;
    console.log(target.value);
  };

  const handleChange = (e: Event) => {
    const target = e.target as HTMLSelectElement;
    console.log(target.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onInput={handleInput} />
      <select onChange={handleChange}>
        <option value="1">Option 1</option>
        <option value="2">Option 2</option>
      </select>
    </form>
  );
}

4.2 鼠标事件 #

tsx
function Button() {
  const handleClick = (e: MouseEvent) => {
    console.log('Clicked at', e.clientX, e.clientY);
  };

  const handleMouseEnter = (e: MouseEvent) => {
    console.log('Mouse entered');
  };

  return (
    <button 
      onClick={handleClick}
      onMouseEnter={handleMouseEnter}
    >
      Click me
    </button>
  );
}

4.3 键盘事件 #

tsx
function Input() {
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'Enter') {
      console.log('Enter pressed');
    }
    if (e.key === 'Escape') {
      console.log('Escape pressed');
    }
  };

  return <input onKeyDown={handleKeyDown} />;
}

五、Context 类型 #

5.1 定义 Context #

tsx
import { createContext } from 'preact';
import { useContext } from 'preact/hooks';

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

const ThemeContext = createContext<ThemeContextType | null>(null);

function ThemeProvider({ children }: { children: preact.ComponentChildren }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  };

  const value: ThemeContextType = {
    theme,
    toggleTheme
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

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

六、类型声明文件 #

6.1 全局类型 #

typescript
// src/types/global.d.ts

interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  image: string;
}

interface ApiResponse<T> {
  data: T;
  success: boolean;
  message?: string;
}

6.2 模块类型 #

typescript
// src/types/api.ts

export interface LoginRequest {
  email: string;
  password: string;
}

export interface LoginResponse {
  token: string;
  user: User;
}

export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}

七、类型工具 #

7.1 组件 Props 类型 #

tsx
// 基础 Props
type BaseProps = {
  className?: string;
  style?: preact.JSX.CSSProperties;
  children?: preact.ComponentChildren;
};

// 扩展 Props
type ButtonProps = BaseProps & {
  variant: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick?: (e: MouseEvent) => void;
};

7.2 工具类型 #

tsx
// 可选 Props
type OptionalProps<T> = {
  [K in keyof T]?: T[K];
};

// 必选 Props
type RequiredProps<T> = {
  [K in keyof T]-?: T[K];
};

// 只读 Props
type ReadonlyProps<T> = {
  readonly [K in keyof T]: T[K];
};

八、最佳实践 #

8.1 类型导出 #

tsx
// types/index.ts
export type { User, Product, Order } from './models';
export type { ApiResponse, PaginatedResponse } from './api';

// components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';

8.2 严格模式 #

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

九、总结 #

要点 说明
FunctionalComponent 组件类型
useState 状态类型
useRef 引用类型
Event 事件类型
createContext Context 类型

核心原则:

  • 使用严格模式
  • 定义清晰的接口
  • 导出类型定义
  • 避免使用 any
最后更新:2026-03-28