Type System

TypeScript-first content modeling with full type safety

Overview

Helix uses TypeScript 5.3+ as its primary type system, enabling you to define content models using familiar TypeScript syntax. The type system is designed to provide maximum type safety while maintaining simplicity and developer ergonomics.

TypeScript Configuration

Helix requires specific TypeScript compiler options to enable decorator support:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true,
    "strictNullChecks": true
  }
}

Make sure experimentalDecorators and emitDecoratorMetadata are enabled. Helix uses the reflect-metadata library for runtime type information.

Primitive Types

Helix supports standard TypeScript primitive types:

export class Product {
  // String
  name!: string;

  // Number
  price!: number;
  quantity!: number;

  // Boolean
  inStock!: boolean;
  featured?: boolean;

  // Date (date only, no time)
  releaseDate?: Date;

  // Optional fields use the ? syntax
  description?: string;
}

Date vs DateTime

By default, Date fields render as date pickers (date only). To include time selection, use the @Widget('datetime') decorator:

export class Event {
  date?: Date; // Date only

  @Widget('datetime')
  startsAt?: Date; // Date and time
}

Field Nullability

TypeScript's strictNullChecks is enforced. Use the ! operator for required fields and ? for optional fields.

Arrays and Collections

Arrays are fully supported and can contain primitives, Components, or Entity references:

export class BlogPost {
  tags!: string[];              // Array of strings
  ratings!: number[];           // Array of numbers
  sections!: Section[];         // Array of Components (embedded)
  relatedPosts!: BlogPost[];   // Array of Entity references (relations)
  authors!: Author[];           // Array of Entity references
}

Special Types

Helix provides special types for common CMS use cases.

Slug

A URL-friendly unique identifier:

import { Slug } from '@helix/sdk';

export class Page {
  @Slug()
  slug!: Slug;

  title!: string;
}

The Slug type is a branded string that indicates this field should be treated as a slug. It's automatically validated for uniqueness and URL safety.

Asset

A reference to an uploaded file (image, video, document, etc.):

import { Asset } from '@helix/sdk';

export class Product {
  name!: string;

  // Single asset
  image?: Asset;

  // Multiple assets
  gallery?: Asset[];
}

The Asset type is a branded string that represents a reference to the internal _Asset entity. The Admin Studio renders an asset picker for these fields.

JSON / Object

For arbitrary JSON data, use the object or any type with the @Widget('json') decorator:

export class Config {
  name!: string;

  @Widget('json')
  metadata?: Record<string, any>;

  @Widget('json')
  settings?: {
    theme: string;
    layout: string;
  };
}

Enums and String Literals

Enums and string literal unions automatically render as select dropdowns in the Admin Studio.

Enum Type

enum PostStatus {
  Draft = 'draft',
  Published = 'published',
  Archived = 'archived',
}

export class BlogPost {
  title!: string;
  status!: PostStatus; // Renders as dropdown
}

String Literal Union

export class Article {
  title!: string;

  // Renders as single-select dropdown
  category!: 'technology' | 'design' | 'business';

  // Multi-select with @Widget decorator
  @Widget({ component: 'select', many: true })
  tags?: ('typescript' | 'react' | 'nodejs')[];
}

The schema extractor automatically detects enums and string literal unions and converts them to select fields with the appropriate choices.

Relations

Entity references are automatically detected as relations:

export class Article {
  @Localized()
  title!: string;

  // Single relation - inferred from Entity type
  author?: Author;

  // Many relation - inferred from Entity array type
  tags?: Tag[];

  // Self-referential relation
  relatedArticles?: Article[];
}

export class Author {
  name!: string;
}

export class Tag {
  name!: string;
}

No decorators needed! Helix automatically detects that Author and Tag are exported classes (Entities) and creates relations in the edges table. Array types automatically become many-to-many relations.

Union Types (Dynamic Zones)

Union type arrays are automatically detected as Dynamic Zones, allowing polymorphic content blocks:

// Components (non-exported)
class Hero {
  @Localized()
  title!: string;
  subtitle?: string;
  background?: Asset;
}

class TextBlock {
  @Localized()
  @Widget('richtext')
  content!: string;
}

class Gallery {
  images!: Asset[];
}

// Entities (exported)
export class Feature {
  name!: string;
  icon?: Asset;
}

// Entity with Dynamic Zone
export class Page {
  @Localized()
  title!: string;

  // Union type array - automatically detected as Dynamic Zone
  @Localized()
  sections!: (Hero | TextBlock | Gallery | Feature)[];
}

No @Zone() decorator needed! Helix automatically detects union type arrays and creates a Dynamic Zone that can hold any mix of the specified Components and Entity references.

Type Mapping

Here's how TypeScript types map to Helix field types:

TypeScript TypeHelix Field TypeAdmin UI Widget
stringstringText input
numbernumberNumber input
booleanbooleanSwitch/toggle
DatedateDate picker
SlugslugSlug field (auto-generated)
AssetrelationAsset picker
string[]arrayString list
EnumTypeselectDropdown
'a' | 'b' | 'c'selectDropdown
ComponentClassobjectNested form
ComponentClass[]object (many)Nested form list
EntityClassrelation (auto)Entity picker
EntityClass[]relation (auto, many)Entity multi-picker
(A | B | C)[]dynamic_zone (auto)Zone builder
Record<string, any>jsonJSON editor

Relations and Dynamic Zones are automatically inferred from your TypeScript types. When you reference an exported class, Helix knows it's a relation. When you use a union type array, Helix knows it's a Dynamic Zone. No decorators required!

Type Safety in the SDK

When using the @helix/client SDK, your TypeScript types are preserved throughout the query pipeline:

import { getClient, tables } from '@helix/client';
import * as schema from './types';

const client = getClient();
const { pages, articles } = tables(schema);

// Full type safety - IDE autocomplete works!
const posts = await client
  .select({
    title: articles.title,        // TypeScript knows this is a string
    slug: articles.slug,           // TypeScript knows this is a Slug
    authorName: articles.author.name, // Dotted access is type-safe
  })
  .from(articles)
  .execute({ locale: 'en' });

// `posts` is typed as Array<{ title: string; slug: Slug; authorName: string }>

Best Practices

  • Use strict mode: Enable strict: true in your tsconfig.json
  • Prefer required fields: Use ! for fields that must always have a value
  • Keep schemas in one file: Store all types in a single src/types.ts file for clarity
  • Use descriptive names: Class and field names appear directly in the Admin UI
  • Leverage unions: Use union types with @Zone() for flexible, polymorphic content
  • Use the Asset type: For file uploads, always use Asset instead of string
  • Use enums for choices: Enums and string literal unions automatically create select fields
  • Combine with decorators: Use @Localized(), @Require(), @Description() to enhance types