TypeScript JSX Support
Introduction
JSX (JavaScript XML) is a syntax extension that allows you to write HTML-like code within JavaScript. Made popular by React, JSX provides a powerful way to define UI components. TypeScript offers full support for JSX, allowing you to combine the benefits of static typing with the expressiveness of JSX syntax.
In this guide, you'll learn:
- What TypeScript JSX support means and how it works
- How to configure TypeScript for JSX
- How type checking works with JSX elements
- Best practices for using TypeScript with JSX
Understanding TypeScript JSX Support
JSX extends JavaScript by allowing you to use XML-like syntax directly in your code. TypeScript enhances JSX by adding type checking to your components, props, and events.
When you use TypeScript with JSX, you'll typically save files with a .tsx
extension rather than .ts
. This signals to the TypeScript compiler that the file contains JSX code.
Setting Up TypeScript for JSX
Configuration Options
To use JSX in TypeScript, you need to configure the jsx
option in your tsconfig.json
file:
{
"compilerOptions": {
"jsx": "react",
// Other options...
}
}
TypeScript supports several JSX modes:
Mode | Description | Output |
---|---|---|
preserve | Keeps JSX as part of output to be processed later | .jsx file with JSX syntax |
react | Converts JSX to React.createElement calls | .js file with React calls |
react-jsx | Transforms JSX for React 17+ new JSX transform | .js file using _jsx calls |
react-jsxdev | Same as react-jsx but with development checks | .js file with development JSX transforms |
react-native | Keeps JSX but outputs .js files | .js file with JSX syntax |
Basic Setup Example
Here's a simple example of setting up a TypeScript React project:
- Install the necessary packages:
npm install react react-dom typescript @types/react @types/react-dom
- Create a
tsconfig.json
file:
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}
Writing TypeScript JSX Code
Basic Component Example
Here's a simple React component written with TypeScript:
import React from 'react';
// Define the props interface
interface GreetingProps {
name: string;
age?: number; // Optional prop
}
// Function component with typed props
const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age !== undefined && <p>You are {age} years old.</p>}
</div>
);
};
// Usage
export const App: React.FC = () => {
return (
<div>
<Greeting name="Alice" age={30} />
<Greeting name="Bob" />
</div>
);
};
Class Component Example
Class components can also be strongly typed:
import React, { Component } from 'react';
interface CounterProps {
initialCount: number;
}
interface CounterState {
count: number;
}
class Counter extends Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = {
count: props.initialCount
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
}
decrement = () => {
this.setState({ count: this.state.count - 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
);
}
}
// Usage
export const App: React.FC = () => {
return <Counter initialCount={0} />;
};
Advanced TypeScript JSX Features
Type Checking for Event Handlers
TypeScript can help ensure you use the correct event types:
import React from 'react';
interface FormProps {
onSubmit: (data: { username: string; password: string }) => void;
}
const LoginForm: React.FC<FormProps> = ({ onSubmit }) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const username = formData.get('username') as string;
const password = formData.get('password') as string;
onSubmit({ username, password });
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
);
};
Generic Components
TypeScript allows you to create reusable components with generics:
import React from 'react';
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage
export const App: React.FC = () => {
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
return (
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
/>
);
};
JSX Factory Customization
TypeScript allows you to customize the JSX factory function used for transforming JSX elements. This is useful for libraries like Preact that use a different function than React's createElement
.
Custom JSX Pragma
You can specify a custom JSX factory in individual files using the JSX pragma comment:
/** @jsx h */
import { h } from 'preact';
const element = <div>Hello, world!</div>;
// Transforms to: const element = h('div', null, 'Hello, world!');
Or globally in your tsconfig.json
:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h"
}
}
Real-World Application Example
Let's build a more comprehensive example: a type-safe to-do list application.
import React, { useState } from 'react';
// Define our types
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
title: string;
}
// Main component
const TodoApp: React.FC<TodoListProps> = ({ title }) => {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodo, setNewTodo] = useState<string>('');
const addTodo = (e: React.FormEvent) => {
e.preventDefault();
if (newTodo.trim() === '') return;
const newTodoItem: Todo = {
id: Date.now(),
text: newTodo,
completed: false
};
setTodos([...todos, newTodoItem]);
setNewTodo('');
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="todo-app">
<h1>{title}</h1>
<form onSubmit={addTodo}>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new task"
/>
<button type="submit">Add</button>
</form>
<ul className="todo-list">
{todos.length === 0 ? (
<li className="empty-state">No tasks yet! Add one above.</li>
) : (
todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))
)}
</ul>
<div className="todo-stats">
<p>Total tasks: {todos.length}</p>
<p>Completed: {todos.filter(todo => todo.completed).length}</p>
<p>Remaining: {todos.filter(todo => !todo.completed).length}</p>
</div>
</div>
);
};
// Usage
export const App: React.FC = () => {
return <TodoApp title="My TypeScript Todo List" />;
};
Common Pitfalls and Solutions
Children Props
Working with children in TypeScript requires proper typing:
// ❌ Incorrect: Missing children type
interface PanelProps {
title: string;
}
// ✅ Correct: Using React.PropsWithChildren utility type
interface PanelProps {
title: string;
}
const Panel: React.FC<React.PropsWithChildren<PanelProps>> = ({ title, children }) => {
return (
<div className="panel">
<h2>{title}</h2>
<div className="panel-content">{children}</div>
</div>
);
};
Element Attribute Types
TypeScript helps you catch errors with DOM element properties:
// ❌ TypeScript will catch this error
const Button: React.FC = () => {
return <button disable={true}>Click me</button>; // Error: 'disable' does not exist on type 'ButtonHTMLAttributes'
};
// ✅ Correct usage
const Button: React.FC = () => {
return <button disabled={true}>Click me</button>; // Works correctly
};
Working with Libraries
Component Libraries
When using component libraries like Material UI, you can leverage their TypeScript definitions:
import React from 'react';
import { Button, TextField } from '@mui/material';
interface LoginFormProps {
onSubmit: (username: string, password: string) => void;
}
const LoginForm: React.FC<LoginFormProps> = ({ onSubmit }) => {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(username, password);
};
return (
<form onSubmit={handleSubmit}>
<TextField
label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
fullWidth
margin="normal"
required
/>
<TextField
type="password"
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
fullWidth
margin="normal"
required
/>
<Button variant="contained" color="primary" type="submit">
Login
</Button>
</form>
);
};
Performance Optimization with TypeScript and React
TypeScript can help with React performance optimizations by ensuring you properly type your memoization:
import React, { useMemo, useState } from 'react';
interface User {
id: number;
name: string;
}
interface UserListProps {
users: User[];
filterText: string;
}
const UserList: React.FC<UserListProps> = ({ users, filterText }) => {
// Type-safe memoization
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(filterText.toLowerCase())
);
}, [users, filterText]);
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
// Usage
export const App: React.FC = () => {
const [filterText, setFilterText] = useState('');
const users: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
return (
<div>
<input
type="text"
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
placeholder="Filter users..."
/>
<UserList users={users} filterText={filterText} />
</div>
);
};
Summary
TypeScript's JSX support brings powerful type checking to your React (or other JSX-based framework) applications. By using TypeScript with JSX, you can:
- Catch errors at compile-time rather than runtime
- Improve developer experience with better tooling and autocompletion
- Create self-documenting components with explicit prop interfaces
- Ensure correct usage of components through type validation
The .tsx
file extension and proper configuration in your tsconfig.json
are essential for working with TypeScript and JSX. By leveraging interfaces for props and state, you can make your components safer and more maintainable.
Exercises
-
Create a simple counter component with TypeScript that uses both
useState
and custom event handlers with proper typing. -
Convert a plain JavaScript React component to TypeScript, adding appropriate interfaces for props and state.
-
Create a generic
List
component that can display any type of items with proper typing. -
Implement a form with TypeScript that validates inputs and has typed submission handling.
-
Build a small application using TypeScript and JSX that fetches data from an API and displays it with proper type definitions.
Additional Resources
With TypeScript's JSX support, you can create robust React applications that are easier to maintain and refactor. The static typing helps catch errors early and provides better documentation for your components.
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)