Skip to main content
Preact has first-class TypeScript support with complete type definitions included in the core package. No additional @types packages are needed.

Setup

Preact’s TypeScript definitions are included by default. Just install Preact and start using TypeScript:
1
Install Preact
2
npm install preact
3
Configure TypeScript
4
Create or update your tsconfig.json:
5
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
6
Start coding
7
Create a .tsx file and start building:
8
import { render } from 'preact';

function App() {
  return <h1>Hello TypeScript!</h1>;
}

render(<App />, document.getElementById('app')!);

Type Definitions

Preact’s type definitions are located in the source code:
  • src/index.d.ts - Core Preact types
  • src/jsx.d.ts - JSX type definitions
  • hooks/src/index.d.ts - Hooks type definitions
  • compat/src/index.d.ts - React compatibility types
Reference: src/index.d.ts, hooks/src/index.d.ts

Component Types

Function components are typed using the FunctionComponent type:
import { FunctionComponent } from 'preact';

interface Props {
  name: string;
  age?: number;
}

const Greeting: FunctionComponent<Props> = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>Age: {age}</p>}
    </div>
  );
};
Or use the shorthand (recommended):
interface Props {
  name: string;
  age?: number;
}

function Greeting({ name, age }: Props) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>Age: {age}</p>}
    </div>
  );
}

Props with Children

The ComponentChildren type represents valid children:
import { ComponentChildren } from 'preact';

interface Props {
  title: string;
  children: ComponentChildren;
}

function Card({ title, children }: Props) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}
Alternatively, use FunctionComponent which includes children automatically:
import { FunctionComponent } from 'preact';

interface Props {
  title: string;
}

// children is available automatically
const Card: FunctionComponent<Props> = ({ title, children }) => (
  <div className="card">
    <h2>{title}</h2>
    <div>{children}</div>
  </div>
);

Hooks with TypeScript

import { useState } from 'preact/hooks';

// Type is inferred
function Counter() {
  const [count, setCount] = useState(0);
  // count: number, setCount: (value: number) => void
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

// Explicit type
interface User {
  name: string;
  email: string;
}

function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  
  return (
    <div>
      {user ? <p>{user.name}</p> : <p>Loading...</p>}
    </div>
  );
}

Event Handlers

Preact provides specific event types for all DOM events:
import { JSX } from 'preact';

function Form() {
  const handleSubmit = (e: JSX.TargetedEvent<HTMLFormElement, Event>) => {
    e.preventDefault();
    console.log('Form submitted');
  };

  const handleChange = (e: JSX.TargetedEvent<HTMLInputElement, Event>) => {
    console.log('Input value:', e.currentTarget.value);
  };

  const handleClick = (e: JSX.TargetedMouseEvent<HTMLButtonElement>) => {
    console.log('Button clicked at', e.clientX, e.clientY);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleChange} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}
Common event types:
  • JSX.TargetedEvent<T, E> - Generic event
  • JSX.TargetedMouseEvent<T> - Mouse events
  • JSX.TargetedKeyboardEvent<T> - Keyboard events
  • JSX.TargetedFocusEvent<T> - Focus events

Refs with TypeScript

import { Component } from 'preact';
import { createRef } from 'preact';

class VideoPlayer extends Component {
  play() {
    console.log('Playing...');
  }

  pause() {
    console.log('Paused');
  }
}

class VideoController extends Component {
  playerRef = createRef<VideoPlayer>();

  handlePlay = () => {
    this.playerRef.current?.play();
  };

  handlePause = () => {
    this.playerRef.current?.pause();
  };

  render() {
    return (
      <div>
        <VideoPlayer ref={this.playerRef} />
        <button onClick={this.handlePlay}>Play</button>
        <button onClick={this.handlePause}>Pause</button>
      </div>
    );
  }
}

Context with TypeScript

import { createContext } from 'preact';
import { useContext, useState } from 'preact/hooks';

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

interface AuthContextValue {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

export function AuthProvider({ children }: { children: ComponentChildren }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    });
    const userData = await response.json();
    setUser(userData);
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

Custom Hooks

Type your custom hooks like regular functions:
import { useState, useEffect } from 'preact/hooks';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null
  });

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

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled) {
          setState({ data: null, loading: false, error });
        }
      });

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

  return state;
}

// Usage
interface User {
  id: number;
  name: string;
}

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

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return <div>No data</div>;

  return <div>{data.name}</div>;
}

Generic Components

Create reusable components that work with any type:
import { ComponentChildren } from 'preact';

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => ComponentChildren;
  keyExtractor: (item: T) => string | number;
}

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

// Usage
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

function App() {
  return (
    <List
      items={users}
      renderItem={user => <span>{user.name}</span>}
      keyExtractor={user => user.id}
    />
  );
}

Utility Types

Preact exports useful utility types:
import {
  ComponentChildren,
  ComponentType,
  VNode,
  RefObject,
  ComponentProps
} from 'preact';

// Get props type from a component
const Button: FunctionComponent<{ label: string }> = ({ label }) => (
  <button>{label}</button>
);

type ButtonProps = ComponentProps<typeof Button>;
// { label: string; children?: ComponentChildren }

// RefObject type
const ref: RefObject<HTMLDivElement> = { current: null };

// VNode type
const vnode: VNode = <div>Hello</div>;

Best Practices

1
Enable strict mode
2
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "noImplicitAny": true
  }
}
3
Use inference when possible
4
// Good: Type is inferred
const [count, setCount] = useState(0);

// Unnecessary: Type already inferred
const [count, setCount] = useState<number>(0);

// Necessary: Complex type or null initial value
const [user, setUser] = useState<User | null>(null);
5
Create interfaces for props
6
// Good: Reusable interface
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
}

function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
  return (
    <button className={variant} onClick={onClick}>
      {label}
    </button>
  );
}
7
Use discriminated unions for state
8
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function UserProfile() {
  const [state, setState] = useState<AsyncState<User>>({ 
    status: 'idle' 
  });

  if (state.status === 'loading') return <div>Loading...</div>;
  if (state.status === 'error') return <div>{state.error.message}</div>;
  if (state.status === 'success') return <div>{state.data.name}</div>;
  return null;
}

Next Steps

Hooks

Learn about hooks with full TypeScript support

Context

Type-safe context with TypeScript

Build docs developers (and LLMs) love