10.1 Component Types — Function Component Signatures
Declaring React Component Types in TypeScript
There are two ways to write React function components in TypeScript.
Method 1: FC (FunctionComponent) Type
import React, { FC } from 'react';
interface GreetingProps {
name: string;
age?: number;
}
const Greeting: FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>Age: {age}</p>}
</div>
);
};
Method 2: Direct Props Type Declaration (Recommended)
interface GreetingProps {
name: string;
age?: number;
}
function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>Age: {age}</p>}
</div>
);
}
// Arrow function version
const Greeting = ({ name, age }: GreetingProps) => (
<div>
<h1>Hello, {name}!</h1>
{age && <p>Age: {age}</p>}
</div>
);
FC vs Direct Props Type Comparison
| Item | FC<Props> | Direct (props: Props) |
|---|---|---|
Auto children | ❌ Removed in React 18 | ❌ Must add explicitly |
| Return type inference | ✅ ReactElement | null | ✅ Auto-inferred |
| displayName support | ✅ | ✅ (separate setup) |
| Generic components | Awkward | ✅ Natural |
| Community preference | Divided | ✅ Increasingly preferred |
Since React 18, FC no longer auto-includes children, so explicit declaration is clearer.
ReactNode vs ReactElement vs JSX.Element
import React, { ReactNode, ReactElement } from 'react';
// ReactNode: widest type (includes strings, numbers, null, ReactElement, etc.)
interface ContainerProps {
children: ReactNode; // Recommended: accepts all renderable values
}
// ReactElement: only React elements (no strings/numbers)
interface WrapperProps {
child: ReactElement; // Only JSX elements
}
// JSX.Element: alias for ReactElement<any, any> (less strict)
interface IconProps {
icon: JSX.Element; // Only JSX
}
// Usage examples
function Container({ children }: { children: ReactNode }) {
return <div className="container">{children}</div>;
}
// ✅ All allowed
<Container>text</Container>
<Container>{42}</Container>
<Container><span>JSX</span></Container>
<Container>{null}</Container>
Explicit Return Types
TypeScript infers return types automatically in most cases, but you can declare them explicitly.
import { ReactElement, ReactNode } from 'react';
// Explicit return type (optional)
function Button({ label }: { label: string }): ReactElement {
return <button>{label}</button>;
}
// Component that can return null
function ConditionalComponent({ show }: { show: boolean }): ReactElement | null {
if (!show) return null;
return <div>Visible</div>;
}
Component Type Patterns
Basic Component
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}
function Button({
label,
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
}
Component Accepting Children
import { ReactNode } from 'react';
interface CardProps {
title: string;
children: ReactNode;
footer?: ReactNode;
}
function Card({ title, children, footer }: CardProps) {
return (
<div className="card">
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Usage
<Card title="User Info" footer={<button>Save</button>}>
<p>Content goes here.</p>
</Card>
Polymorphic Component (as prop)
A component that can render as different HTML tags.
import { ComponentPropsWithoutRef, ElementType } from 'react';
type TextProps<T extends ElementType> = {
as?: T;
children: React.ReactNode;
} & ComponentPropsWithoutRef<T>;
function Text<T extends ElementType = 'p'>({
as,
children,
...props
}: TextProps<T>) {
const Component = as ?? 'p';
return <Component {...props}>{children}</Component>;
}
// Usage — correct prop types applied per tag
<Text>Default paragraph</Text> // <p>
<Text as="h1">Heading</Text> // <h1>
<Text as="a" href="/about">Link</Text> // <a href="/about">
<Text as="button" onClick={() => {}}>Button</Text> // <button>
Compound Component Pattern
import { createContext, useContext, ReactNode } from 'react';
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() {
const context = useContext(TabsContext);
if (!context) throw new Error('useTabs must be used within Tabs');
return context;
}
// Main component
interface TabsProps {
defaultTab: string;
children: ReactNode;
}
function Tabs({ defaultTab, children }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
// Sub-components
Tabs.Tab = function Tab({ id, label }: { id: string; label: string }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
className={activeTab === id ? 'active' : ''}
onClick={() => setActiveTab(id)}
>
{label}
</button>
);
};
Tabs.Panel = function Panel({ id, children }: { id: string; children: ReactNode }) {
const { activeTab } = useTabs();
if (activeTab !== id) return null;
return <div className="tab-panel">{children}</div>;
};
// Usage
<Tabs defaultTab="profile">
<Tabs.Tab id="profile" label="Profile" />
<Tabs.Tab id="settings" label="Settings" />
<Tabs.Panel id="profile"><ProfileContent /></Tabs.Panel>
<Tabs.Panel id="settings"><SettingsContent /></Tabs.Panel>
</Tabs>
Pro Tips
1. Extend existing component types with ComponentProps
import { ComponentProps } from 'react';
// All button props + additional props
type EnhancedButtonProps = ComponentProps<'button'> & {
isLoading?: boolean;
loadingText?: string;
};
function EnhancedButton({ isLoading, loadingText = 'Loading...', children, ...props }: EnhancedButtonProps) {
return (
<button disabled={isLoading} {...props}>
{isLoading ? loadingText : children}
</button>
);
}
2. Extract custom component types with ComponentProps<typeof Component>
// Reuse Button component's Props type
type ButtonProps = ComponentProps<typeof Button>;
// Extract a specific prop
type ButtonVariant = ComponentProps<typeof Button>['variant'];