Skip to content

transform.types.ts

The transform.types.ts file contains TypeScript interface definitions for transformed data structures. These types define the clean, UI-ready shape of data after transformation from GraphQL responses.

src/pages/{routeName}/_graphql/transform.types.ts

Examples:

src/pages/articles/_graphql/transform.types.ts
src/pages/places/_graphql/transform.types.ts
src/pages/articles/[slug]/_graphql/transform.types.ts
src/components/{feature}/graphql/transform.types.ts

Examples:

src/components/relatedArticles/graphql/transform.types.ts
src/components/hero/graphql/transform.types.ts

The transform.types.ts file:

  1. Defines UI-ready interfaces - Clean types for component consumption
  2. Simplifies GraphQL types - Removes GraphQL-specific complexity
  3. Documents data shape - Clear contract between API and UI
  4. Enables type safety - TypeScript validation throughout the app
  5. Separates concerns - Decouples UI types from API types
Transformed{Entity}
{Entity}Data
{Feature}Props

Examples:

export interface TransformedArticle { }
export interface HeroData { }
export interface ContentBlock { }
export interface ArticleMetadata { }
// Main page/component data interface
export interface TransformedArticle {
title: string;
slug: string;
publishDate: string;
hero: HeroData;
content: ContentBlock[];
metadata: ArticleMetadata;
relatedArticles: ArticleListItem[];
}
// Nested data structures
export interface HeroData {
title: string;
subtitle: string | null;
image: ImageData | null;
cta: CTAData | null;
}
export interface ImageData {
url: string;
alt: string;
width: number;
height: number;
}
export interface CTAData {
text: string;
url: string;
variant: "primary" | "secondary";
}
// Content blocks with discriminated unions
export type ContentBlock =
| TextBlock
| ImageBlock
| VideoBlock
| QuoteBlock;
export interface TextBlock {
type: "text";
content: string;
}
export interface ImageBlock {
type: "image";
image: ImageData;
caption: string | null;
}
// Array item types
export interface ArticleListItem {
title: string;
slug: string;
excerpt: string;
image: ImageData | null;
publishDate: string;
}
// Metadata and supporting types
export interface ArticleMetadata {
tags: string[];
category: string;
readTime: number;
author: AuthorData;
}
export interface AuthorData {
name: string;
slug: string;
image: ImageData | null;
}

Choose clear, specific names that describe the data:

// ✅ Good: Clear and specific
export interface TransformedArticle { }
export interface HeroData { }
export interface ContentBlock { }
// ❌ Bad: Vague or generic
export interface Data { }
export interface Info { }
export interface Response { }

Add JSDoc comments for complex or non-obvious types:

/**
* Represents a content block in an article.
* Uses discriminated union with `type` field for type safety.
*/
export type ContentBlock =
| TextBlock
| ImageBlock
| VideoBlock;
/**
* Article metadata including SEO and social sharing information.
*/
export interface ArticleMetadata {
/** Page title for SEO */
title: string;
/** Meta description for search engines */
description: string;
/** Open Graph image for social sharing */
ogImage: ImageData | null;
}

For content with multiple variants, use discriminated unions:

// ✅ Good: Type-safe union with discriminant
export type ContentBlock =
| { type: "text"; content: string }
| { type: "image"; image: ImageData; caption: string }
| { type: "video"; videoUrl: string; thumbnail: ImageData };
// Usage provides type narrowing
function renderBlock(block: ContentBlock) {
switch (block.type) {
case "text":
return block.content; // TypeScript knows this is string
case "image":
return block.image.url; // TypeScript knows this exists
case "video":
return block.videoUrl; // TypeScript knows this exists
}
}
// ❌ Bad: Untagged union without discriminant
export type ContentBlock =
| TextBlock
| ImageBlock
| VideoBlock;

Use interfaces for object shapes (better error messages, extendable):

// ✅ Good: Interface for object shape
export interface TransformedArticle {
title: string;
content: ContentBlock[];
}
// ✅ Good: Type alias for unions
export type ContentBlock = TextBlock | ImageBlock;
// ❌ Less ideal: Type for object shape
export type TransformedArticle = {
title: string;
content: ContentBlock[];
};

Be explicit about which fields can be null:

// ✅ Good: Clear null handling
export interface ArticleData {
title: string; // Required
subtitle: string | null; // Explicitly nullable
image: ImageData | null; // Explicitly nullable
tags: string[]; // Required (can be empty array)
}
// ❌ Bad: Unclear nullability
export interface ArticleData {
title: string;
subtitle?: string; // Undefined vs null is ambiguous
image: ImageData; // Looks required but might be null from API
}

Organize types logically with comments:

// === Main Data Types ===
export interface TransformedArticle { }
export interface TransformedPlace { }
// === Content Block Types ===
export type ContentBlock = TextBlock | ImageBlock | VideoBlock;
export interface TextBlock { }
export interface ImageBlock { }
export interface VideoBlock { }
// === Metadata Types ===
export interface ArticleMetadata { }
export interface PlaceMetadata { }
// === Shared Component Types ===
export interface ImageData { }
export interface CTAData { }
export interface AuthorData { }

Define specific types for list items:

// Full article type
export interface TransformedArticle {
title: string;
content: string;
// ... many more fields
}
// Minimal type for list views
export interface ArticleListItem {
title: string;
slug: string;
excerpt: string;
image: ImageData | null;
publishDate: string;
}
// Can extend for different contexts
export interface ArticleFeatured extends ArticleListItem {
featured: true;
priority: number;
}

Define types for reusable UI components:

// Generic image type used across the app
export interface ImageData {
url: string;
alt: string;
width: number;
height: number;
}
// Generic link/CTA type
export interface CTAData {
text: string;
url: string;
variant: "primary" | "secondary" | "text";
external?: boolean;
}
// Generic tag type
export interface TagData {
slug: string;
title: string;
color?: string;
}

Handle deeply nested data with intermediate types:

export interface TransformedArticle {
title: string;
hero: HeroData;
sections: SectionData[];
}
export interface HeroData {
title: string;
background: BackgroundData;
overlay: OverlayData | null;
}
export interface BackgroundData {
type: "image" | "video";
image?: ImageData;
video?: VideoData;
}
export interface OverlayData {
opacity: number;
color: string;
}

Transform types are distinct from generated GraphQL types:

// Generated type from GraphQL Codegen (complex, nullable fields)
import type { ArticlePageQuery } from "#graphql/generated/contentful/schema";
// Transform type (clean, UI-ready)
export interface TransformedArticle {
title: string;
content: ContentBlock[];
}
// Transform function bridges the two
export function transformArticleData(
data: ArticlePageQuery
): TransformedArticle {
// Convert from generated type to transform type
}