Skip to content

Manual Typing

Typelizer infers types from your database columns and associations automatically. When you have computed attributes, custom types, or need finer control, use the typelize method to specify types manually.

Typing a Single Attribute

Place typelize before an attribute to annotate its type:

ruby
class PostResource < ApplicationResource
  attributes :id, :title, :body, :published_at

  typelize "string"
  attribute :author_name do |post|
    post.author.name
  end

  typelize :string, nullable: true, comment: "Author's avatar URL"
  attribute :avatar do
    "https://example.com/avatar.png" if active?
  end
end

This generates:

typescript
export type Post = {
  id: number;
  title: string;
  body: string;
  published_at: string;
  author_name: string;
  /** Author's avatar URL */
  avatar: string | null;
}

Typing Multiple Attributes

Use a Hash to specify types for several attributes at once:

ruby
class PostResource < ApplicationResource
  attributes :id, :title, :body, :published_at

  attribute :author_name do |post|
    post.author.name
  end

  typelize author_name: :string, published_at: :string
end

Shortcut Syntax

Common type modifiers have shortcut forms:

ruby
class PostResource < ApplicationResource
  typelize author_name: "string?"       # optional string (author_name?: string)
  typelize tag_ids: "number[]"          # array of numbers (tag_ids: Array<number>)
  typelize categories: "string?[]"      # optional array of strings (categories?: Array<string>)

  # Use the `?` suffix on the key as an alternative to the type shortcut
  typelize nickname?: "string"          # same as nickname: "string?"
  typelize avatar_url?: [:string, nullable: true]

  # Shortcuts can be combined with explicit options
  typelize status: [:string?, nullable: true]  # optional and nullable

  # Also works with keyless typelize
  typelize :string?
  attribute :slug do |post|
    post.slug
  end
end

This generates:

typescript
export type Post = {
  author_name?: string;
  tag_ids: Array<number>;
  categories?: Array<string>;
  nickname?: string;
  avatar_url?: string | null;
  status?: string | null;
  slug?: string;
}

TIP

The ? suffix on keys mirrors TypeScript's own syntax and composes with the "string?" type shortcut -- both forms produce the same output, so pick whichever reads better for your attribute.

Explicit options win when they conflict: typelize name?: [:string, optional: false] produces name: string (required).

Inline Object Types

Pass a hash literal with keyless typelize to describe an inline object type -- useful for JSON columns, computed hashes, and ad-hoc shapes that don't warrant a separate resource:

ruby
class PostResource < ApplicationResource
  attributes :id, :title

  typelize({id: :number, label: "string?"})
  attribute :category do |post|
    {id: post.category_id, label: post.category_name}
  end
end

This generates:

typescript
export type Post = {
  id: number;
  title: string;
  category: {
    id: number;
    label?: string;
  };
}

WARNING

Note the parentheses: typelize({...}) passes the hash as a positional argument -- the inline shape form. typelize(key: value) without braces is keyword arguments -- the attribute-typing form. This is the same distinction Ruby itself makes.

Composing with Modifiers

Pass options as the second argument -- they apply to the shape as a whole:

ruby
class PostResource < ApplicationResource
  # Array of shapes
  typelize({id: :number, name: :string}, multi: true)
  attribute :tags

  # Optional + nullable shape
  typelize({street: :string, city: :string}, optional: true, nullable: true)
  attribute :address
end

This generates:

typescript
export type Post = {
  tags: Array<{
    id: number;
    name: string;
  }>;
  address?: {
    street: string;
    city: string;
  } | null;
}

Nested Shapes

Values can themselves be hashes -- nest as deep as you need:

ruby
class OrderResource < ApplicationResource
  attributes :id

  typelize({
    customer: {name: :string, email?: :string},
    totals: {subtotal: :number, tax: :number, grand_total: :number}
  })
  attribute :summary
end

This generates:

typescript
export type Order = {
  id: number;
  summary: {
    customer: {
      name: string;
      email?: string;
    };
    totals: {
      subtotal: number;
      tax: number;
      grand_total: number;
    };
  };
}

Mixing Shapes with Type Shortcuts

Shape values accept anything a regular typelize value accepts, including string shortcuts and unions:

ruby
class FeedItemResource < ApplicationResource
  typelize({
    payload: "TextBlock | ImageBlock",
    tags: "string[]"
  })
  attribute :item
end

This generates:

typescript
export type FeedItem = {
  item: {
    payload: TextBlock | ImageBlock;
    tags: Array<string>;
  };
}

TIP

For serializers using Alba's nested_attribute, Typelizer already infers the shape from the block -- you don't need typelize in that case. Use inline shapes when typing computed attributes, JSON columns, or overriding the inferred shape.

Referencing Other Serializers

Pass a serializer class directly. Typelizer resolves it to the generated TypeScript type name:

ruby
class PostResource < ApplicationResource
  attributes :id, :title

  # Reference another serializer
  typelize reviewer: [AuthorResource, {optional: true, nullable: true}]
  attribute :reviewer do |post|
    post.reviewer
  end

  # Self-reference works too
  typelize previous_post: PostResource
  attribute :previous_post do |post|
    post.previous_post
  end
end

Union Types

For polymorphic associations, use serializer class references or pipe-delimited strings:

ruby
class PostResource < ApplicationResource
  attributes :id, :title

  # Union of two serializers
  typelize commentable: [UserResource, CommentResource]
  attribute :commentable

  # Nullable union -- extracts null and marks as nullable
  typelize approver: "AuthorResource | null"
  attribute :approver

  # Pipe-delimited string with serializer names
  typelize target: "UserResource | CommentResource"
  attribute :target

  # Pipe-delimited string with namespaced serializer
  typelize item: "Namespace::UserResource | CommentResource"
  attribute :item
end

Plain TypeScript type names are passed through as-is:

ruby
class PostResource < ApplicationResource
  attributes :id, :title

  # Plain type names
  typelize content: "TextBlock | ImageBlock"
  attribute :content

  # Works with arrays of symbols too
  typelize sections: [:TextBlock, :ImageBlock]
  attribute :sections
end

This generates:

typescript
type Post = {
  id: number;
  title: string;
  content: TextBlock | ImageBlock;
  sections: TextBlock | ImageBlock;
}

String Literal Unions

Arrays of strings become string literal union types -- useful for enums and state machines:

ruby
class PostResource < ApplicationResource
  attributes :id, :title

  # Array of strings
  typelize status: ["draft", "published", "archived"]
  attribute :status

  # Works with Rails enums and state machines
  typelize review_state: ReviewStateMachine.states.keys
  attribute :review_state
end

This generates:

typescript
type Post = {
  id: number;
  title: string;
  status: 'draft' | 'published' | 'archived';
  review_state: 'pending' | 'approved' | 'rejected';
}

TIP

In arrays, strings become string literal types ('a'), while symbols and class constants become type references (A). You can mix them: [:number, "auto"] produces number | 'auto'.

Full API

The typelize method supports these options:

ruby
typelize attribute_name: [
  :string, :Date,
  optional: true,
  nullable: true,
  multi: true,
  enum: %w[foo bar],
  comment: "Attribute description",
  deprecated: "Use `another_attribute` instead"
]
OptionEffect
optionalMakes the property optional (name?: type). Equivalent to a ? suffix on the attribute key or type shortcut.
nullableAdds null to the type (type | null)
multiWraps in Array<type>
enumGenerates string literal union
commentAdds a JSDoc comment above the property
deprecatedAdds a @deprecated JSDoc tag

For inline object types, pass a positional hash: typelize({key: type, ...}, options). See Inline Object Types.

The behaviour of nullable depends on your null_strategy configuration.