TypeScript

TypeScript for React Developers

Key patterns to type components, hooks, and context without the pain.

November 10, 2025
4 min read
By useLines Team
TypeScriptReactTypesHooks
Illustration for TypeScript for React Developers

TypeScript for React, in short

TypeScript makes React apps safer and easier to work on. You get clear errors, great autocomplete, and self‑documenting code. Start small, add types where they help, and grow from there.

Why it helps

  • Catch mistakes before runtime (e.g., “object is possibly undefined”)
  • Autocomplete for props, state, and API responses
  • Safer refactors and clearer APIs

What to type first

  • Component props and events
  • API responses and form state
  • Reusable utilities (with generics)

Minimal example

interface ButtonProps { label: string; onClick?: () => void }
export function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

Practical tips

  • Prefer interface for objects; type for unions/aliases
  • Avoid any; use unknown or generics
  • Return typed results from API helpers
  • Don’t over‑type; keep it readable

Bottom line

Start with props and API types, then add more where it reduces bugs and improves DX.

export function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = React.useState<T>(() => {
    const raw = localStorage.getItem(key);
    return raw ? (JSON.parse(raw) as T) : initial;
  });
  React.useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue] as const;
}

Context with selectors

Avoid re-rendering all consumers by exposing selector hooks:

type State = { theme: "light" | "dark"; user?: { id: string; name: string } };
const AppState = React.createContext<State | null>(null);

export function useTheme() {
  const ctx = React.useContext(AppState);
  if (!ctx) throw new Error("useTheme must be used within provider");
  return ctx.theme;
}

Component patterns

  • Discriminated unions for variant-heavy components
  • Template literal types for class names and tokens
  • Utility types (Pick, Omit, ReturnType) to compose APIs
type Variant = "primary" | "secondary" | "ghost";
type Size = "sm" | "md" | "lg";

type ButtonProps = {
  variant?: Variant;
  size?: Size;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

export const Button: React.FC<ButtonProps> = ({
  variant = "primary",
  size = "md",
  ...rest
}) => {
  const cls = `btn ${variant} ${size}` as const;
  return <button className={cls} {...rest} />;
};

Testing types

Use tsd or expectTypeOf to lock type contracts in libraries. In apps, favor strict options in tsconfig and keep any out of hot paths.

Conclusion

Type the public surface, expose narrow hooks/selectors, and use unions/generics to keep components expressive and safe.

Advanced patterns

Polymorphic components

type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PolymorphicProps<C extends React.ElementType, P> = AsProp<C> &
  Omit<React.ComponentPropsWithoutRef<C>, keyof AsProp<C>> &
  P;

export function Text<C extends React.ElementType = "span">({
  as,
  children,
  ...rest
}: PolymorphicProps<C, { weight?: "regular" | "bold" }>) {
  const Component = (as || "span") as React.ElementType;
  return <Component {...rest}>{children}</Component>;
}

Exhaustiveness checks

type Status = "idle" | "loading" | "success" | "error";

function assertNever(x: never): never {
  throw new Error(`Unhandled: ${x}`);
}

function statusToColor(s: Status) {
  switch (s) {
    case "idle":
      return "gray";
    case "loading":
      return "blue";
    case "success":
      return "green";
    case "error":
      return "red";
    default:
      return assertNever(s);
  }
}

Inference helpers

Use satisfies to keep widening under control and preserve literal types.

const routes = {
  home: "/",
  blog: "/blog",
  post: (slug: string) => `/post/${slug}`,
} as const satisfies Record<string, unknown>;