Skip to main content
Advertisement

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>
);
};
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

ItemFC<Props>Direct (props: Props)
Auto children❌ Removed in React 18❌ Must add explicitly
Return type inferenceReactElement | null✅ Auto-inferred
displayName support✅ (separate setup)
Generic componentsAwkward✅ Natural
Community preferenceDivided✅ 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'];
Advertisement