@types packages are needed.
Setup
Preact’s TypeScript definitions are included by default. Just install Preact and start using TypeScript:Type Definitions
Preact’s type definitions are located in the source code:src/index.d.ts- Core Preact typessrc/jsx.d.ts- JSX type definitionshooks/src/index.d.ts- Hooks type definitionscompat/src/index.d.ts- React compatibility types
src/index.d.ts, hooks/src/index.d.ts
Component Types
- Function Components
- Class Components
Function components are typed using the Or use the shorthand (recommended):
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>
);
};
interface Props {
name: string;
age?: number;
}
function Greeting({ name, age }: Props) {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>Age: {age}</p>}
</div>
);
}
Class components extend
Component with props and state types:import { Component } from 'preact';
interface Props {
initialCount: number;
}
interface State {
count: number;
}
class Counter extends Component<Props, State> {
state: State = {
count: this.props.initialCount
};
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
Props with Children
TheComponentChildren 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>
);
}
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>
);
}
import { useReducer } from 'preact/hooks';
interface State {
count: number;
error: string | null;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number }
| { type: 'error'; payload: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return { ...state, count: action.payload };
case 'error':
return { ...state, error: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
error: null
});
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
Reset
</button>
</div>
);
}
import { useRef } from 'preact/hooks';
// DOM element ref
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</div>
);
}
// Mutable value ref
function Timer() {
const intervalRef = useRef<number | null>(null);
const start = () => {
intervalRef.current = window.setInterval(() => {
console.log('tick');
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
return (
<div>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
import { createContext } from 'preact';
import { useContext } from 'preact/hooks';
interface Theme {
primary: string;
secondary: string;
}
const ThemeContext = createContext<Theme>({
primary: '#007bff',
secondary: '#6c757d'
});
function ThemedButton() {
const theme = useContext(ThemeContext);
// theme: Theme
return (
<button style={{ backgroundColor: theme.primary }}>
Click me
</button>
);
}
// With undefined default (requires checking)
const AuthContext = createContext<User | undefined>(undefined);
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
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>
);
}
JSX.TargetedEvent<T, E>- Generic eventJSX.TargetedMouseEvent<T>- Mouse eventsJSX.TargetedKeyboardEvent<T>- Keyboard eventsJSX.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>
);
}
}
import { forwardRef } from 'preact/compat';
import { Ref } from 'preact';
interface Props {
label: string;
}
const TextInput = forwardRef<HTMLInputElement, Props>(
({ label }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} type="text" />
</div>
)
);
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<form>
<TextInput ref={inputRef} label="Name" />
<button onClick={() => inputRef.current?.focus()}>
Focus Input
</button>
</form>
);
}
function Component() {
const handleRef = (element: HTMLDivElement | null) => {
if (element) {
console.log('Element mounted:', element);
} else {
console.log('Element unmounted');
}
};
return <div ref={handleRef}>Content</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
Next Steps
Hooks
Learn about hooks with full TypeScript support
Context
Type-safe context with TypeScript