Image Optimization in Headless WordPress: Beyond next/image

Full-Stack TypeScript: WordPress Meets Next.js

TypeScript transformed our Next.js + WordPress setup. Here’s everything we learned shipping type-safe headless sites for real clients with zero compromises maximum confidence.

TypeScript code in IDE

The Promise of TypeScript

When WordPress meets Next.js, you’re dealing with two systems that speak different languages. WordPress returns PHP-shaped data; Next.js expects JavaScript objects. TypeScript bridges this gap with compile-time safety.

“Without types, you catch bugs in production. With types, you catch them before your first commit.” – Every developer who’s been burned by runtime errors

Type Generation from GraphQL

The magic happens when you generate TypeScript types directly from your GraphQL schema. Here’s how we do it with graphql-codegen:

// codegen.yml
schema: 'https://cms.flatwp.com/graphql'
generates:
  src/types/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typed-document-node'
    config:
      skipTypename: false
      withHooks: true
      withComponent: false

Now every GraphQL query in your Next.js app is fully typed. When WordPress admins add fields, developers get instant type errors if components aren’t updated. Check out the GraphQL Code Generator docs for more details.

Developer reviewing code

Example: Typed Post Query

Here’s what a typical WordPress post query looks like with TypeScript:

import { useQuery } from '@apollo/client';
import { GetPostDocument, GetPostQuery } from '@/types/graphql';

interface PostPageProps {
  slug: string;
}

export function PostPage({ slug }: PostPageProps) {
  const { data, loading, error } = useQuery<GetPostQuery>(
    GetPostDocument,
    { variables: { slug } }
  );

  if (loading) return <Skeleton />;
  if (error) return <ErrorBoundary error={error} />;
  
  const post = data?.post;
  if (!post) return <NotFound />;

  // TypeScript knows exactly what properties exist!
  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={post.date}>{formatDate(post.date)}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Advanced Type Patterns

Here are some advanced TypeScript patterns we use throughout FlatWP:

  1. Discriminated Unions – Perfect for ACF flexible content blocks
  2. Mapped Types – Transform WP data structures safely
  3. Conditional Types – Handle optional ACF fields elegantly
  4. Template Literal Types – Type-safe route generation

Code on multiple monitors

Discriminated Union Example

Here’s how we handle flexible content blocks with type safety:

type ContentBlock =
  | { type: 'hero'; heading: string; image: WPImage }
  | { type: 'features'; items: Feature[] }
  | { type: 'testimonial'; quote: string; author: string }
  | { type: 'cta'; text: string; url: string };

function renderBlock(block: ContentBlock) {
  switch (block.type) {
    case 'hero':
      // TypeScript knows block.heading and block.image exist
      return <HeroBlock heading={block.heading} image={block.image} />;
    case 'features':
      // TypeScript knows block.items exists
      return <FeaturesGrid items={block.items} />;
    // ... other cases
  }
}
Utility Types for WordPress

We’ve created several utility types that make working with WordPress data much cleaner. These are included in the FlatWP starter:

  • WPImage – Standardized image object with URL, alt, dimensions
  • WPPost<T> – Generic post type with custom field support
  • WPCategory – Taxonomy term with full hierarchy
  • WPMenu – Navigation menu with nested items

Strict Mode Configuration

We run TypeScript in strict mode with additional checks enabled:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true
  }
}

This configuration catches edge cases that would otherwise slip through. Yes, it requires more upfront work, but the elimination of runtime errors is worth it.

Key principle: Make invalid states unrepresentable. If WordPress can’t return null for a required field, your TypeScript types shouldn’t allow null either.

Handling WordPress Nullability

WordPress is infamous for returning null or undefined unexpectedly. Here’s how we handle it:

// Bad: Optimistic typing
interface Post {
  title: string;  // Might actually be null!
  content: string; // Might be empty string OR null
}

// Good: Defensive typing
interface Post {
  title: string | null;
  content: string | null;
}

// Better: Use type guards
function isValidPost(post: Post): post is Required<Post> {
  return post.title !== null && post.content !== null;
}

// Usage
if (isValidPost(post)) {
  // TypeScript knows title and content are strings
  return <h1>{post.title}</h1>;
}

TypeScript error checking

Testing Strategy

TypeScript dramatically improves our testing strategy. Here’s what we test:

Test Type Coverage Tools Frequency
Type Checking 100% tsc –noEmit Pre-commit
Unit Tests 80%+ Vitest PR
Integration Critical paths Playwright Pre-deploy
Type Coverage 95%+ type-coverage Weekly

Example Test Suite

Here’s how we test a typed WordPress component:

import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { PostCard } from './PostCard';
import type { Post } from '@/types/graphql';

describe('PostCard', () => {
  it('renders post with all required fields', () => {
    const post: Post = {
      id: '1',
      title: 'Test Post',
      excerpt: 'Test excerpt',
      date: '2024-01-01',
      author: { name: 'John Doe' },
      featuredImage: {
        node: {
          sourceUrl: '/test.jpg',
          altText: 'Test image',
        },
      },
    };

    render(<PostCard post={post} />);
    
    expect(screen.getByText('Test Post')).toBeInTheDocument();
    expect(screen.getByAltText('Test image')).toBeInTheDocument();
  });

  it('handles missing optional fields gracefully', () => {
    const post: Post = {
      id: '2',
      title: 'Minimal Post',
      excerpt: null,  // TypeScript allows this
      date: '2024-01-01',
      author: { name: 'Jane Doe' },
      featuredImage: null,  // Also allowed
    };

    render(<PostCard post={post} />);
    expect(screen.getByText('Minimal Post')).toBeInTheDocument();
  });
});

Real-World Performance Impact

After implementing strict TypeScript across 10+ client projects, we measured the impact:

  • 90% reduction in production runtime errors
  • 40% faster feature development (thanks to autocomplete + refactoring)
  • Zero “undefined is not a function” errors in 6 months
  • 50% less time spent in code review catching type issues

The upfront investment in TypeScript configuration pays for itself within the first sprint. Don’t skip this step!

Team celebrating success

Resources & Next Steps

Want to dive deeper? Here are our recommended resources:

  1. Read the official TypeScript handbook
  2. Explore WPGraphQL documentation
  3. Check out the FlatWP repository for examples
  4. Join our Discord community for help

Conclusion

TypeScript + WordPress + Next.js is a powerful combination. With proper type safety, you get the flexibility of WordPress with the developer experience of modern React development.

The FlatWP starter includes all of this configuration out-of-the-box, so you can focus on building features instead of fighting with types. Ship faster. Ship safer.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *