Skip to main content

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:

json
{
"compilerOptions": {
"jsx": "react",
// Other options...
}
}

TypeScript supports several JSX modes:

ModeDescriptionOutput
preserveKeeps JSX as part of output to be processed later.jsx file with JSX syntax
reactConverts JSX to React.createElement calls.js file with React calls
react-jsxTransforms JSX for React 17+ new JSX transform.js file using _jsx calls
react-jsxdevSame as react-jsx but with development checks.js file with development JSX transforms
react-nativeKeeps 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:

  1. Install the necessary packages:
bash
npm install react react-dom typescript @types/react @types/react-dom
  1. Create a tsconfig.json file:
json
{
"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:

tsx
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:

tsx
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:

tsx
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:

tsx
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:

tsx
/** @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:

json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h"
}
}

Real-World Application Example

Let's build a more comprehensive example: a type-safe to-do list application.

tsx
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:

tsx
// ❌ 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:

tsx
// ❌ 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:

tsx
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:

tsx
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

  1. Create a simple counter component with TypeScript that uses both useState and custom event handlers with proper typing.

  2. Convert a plain JavaScript React component to TypeScript, adding appropriate interfaces for props and state.

  3. Create a generic List component that can display any type of items with proper typing.

  4. Implement a form with TypeScript that validates inputs and has typed submission handling.

  5. 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! :)