AvalonAvalon
GitHub

TypeScript

TypeScript setup, JSX pragmas, and type imports in Avalon.

Avalon is built with TypeScript and provides types for all its APIs. Pages, layouts, and islands are all .tsx or .ts files by default.

Type imports

The main types come from @useavalon/avalon:

import type { LayoutProps } from '@useavalon/avalon';

Client-side component types come from @useavalon/avalon/client:

import type { ImageProps } from '@useavalon/avalon/client';

JSX pragmas

Avalon uses Preact as the default JSX runtime for pages and layouts. For islands using other frameworks, add a JSX pragma at the top of the file:

// React island
/** @jsxImportSource react */
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
// Solid island
/** @jsxImportSource solid-js */
import { createSignal } from 'solid-js';

export default function Counter() {
  const [count, setCount] = createSignal(0);
  return <button onClick={() => setCount(c => c + 1)}>Count: {count()}</button>;
}

Preact islands don't need a pragma since it's the default jsxImportSource.

Framework-specific file extensions

Some frameworks use their own file extensions instead of JSX pragmas:

FrameworkExtensionPragma needed?
Preact.tsxNo (default)
React.tsxYes: /** @jsxImportSource react */
Solid.solid.tsxConvention-based, or use pragma
Vue.vueNo
Svelte.svelteNo
Lit.tsNo (uses decorators, not JSX)
Qwik.qwik.tsxConvention-based

tsconfig

A minimal tsconfig.json for an Avalon project:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "strict": true,
    "skipLibCheck": true,
    "paths": {
      "@shared/*": ["./app/shared/*"],
      "@modules/*": ["./app/modules/*"]
    }
  },
  "include": ["app/**/*"]
}

The paths should match the resolve.alias entries in your Vite config.

Typing page props

Pages that use dynamic routes receive an event prop:

interface PageProps {
  event: {
    context: {
      params?: Record<string, string>;
    };
  };
}

export default async function BlogPost({ event }: PageProps) {
  const slug = event.context.params?.slug;
  // ...
}

Typing layout props

import type { LayoutProps } from '@useavalon/avalon';

export default function MyLayout({ children, frontmatter }: LayoutProps) {
  const title = frontmatter?.title as string | undefined;
  // children: ComponentChildren
  // frontmatter: Record<string, unknown>
}

Typing island props

Island props must be serializable (no functions, no class instances). TypeScript helps enforce this:

interface ChartProps {
  data: number[];
  label: string;
  color?: string;
}

export default function Chart({ data, label, color = '#1F6AD3' }: ChartProps) {
  // ...
}