TypeScript
JAF components are HTML files, so there is no .tsx to type-check.
What the build gives you instead is generated declaration files describing your routes,
components and unite() namespaces, which your editor and
tsc pick up automatically.
The Generated Directory
Everything is written to src/generated/.
The directory is gitignored and regenerated on every build and on file changes in dev, so never edit it by hand.
| File | Module | Generated by |
|---|---|---|
routes.d.ts |
jaf:routes |
fileRouterPlugin() - on by default |
namespaces.d.ts |
global augmentation | namespaceCodegenPlugin() - opt in |
components.d.ts |
jaf:components |
componentTypesCodegenPlugin() - see the note below |
Make sure src/generated is inside your tsconfig.json include:
{
"include": ["src", "src/generated"]
}
Route Types
Every route in pages/ gets a generated name and a params type.
The name is the URL path with slashes turned into hyphens and colons removed -
/ is home, /users/:id is users-id.
Params are always string.
// src/generated/routes.d.ts (auto-generated)
declare module "jaf:routes" {
export interface RouteMap {
"home": {}
"about": {}
"blog-slug": { slug: string }
"products-id": { id: string }
}
export type RouteName = keyof RouteMap
export type RouteParams<T extends RouteName> = RouteMap[T]
export type StaticRouteName = /* routes with no params */
export type DynamicRouteName = Exclude<RouteName, StaticRouteName>
}
Import the types wherever you hold a route name:
import type { RouteName, RouteParams } from 'jaf:routes'
const defaultRoute: RouteName = 'home' // checked
const params: RouteParams<'blog-slug'> = { slug: 'hello' }
They also type navigateTo(), which takes a route name rather than
a path and checks the params against it:
import { navigateTo, generatePath } from 'jafjs/router'
navigateTo('users-id', { id: '123' }) // params checked against the route
generatePath('users-id', { id: '123' }) // -> '/users/123'
Most navigation is still an ordinary <a href>, which the
router intercepts.
Typing Route State
window.route is declared globally and is reactive. It is not parameterised
by RouteMap, so params come back as a plain record:
<script>
// Static snapshot
const id = window.route.params.id
// Derived: re-evaluates when the route changes
let slug = window.route.params.slug
</script>
The shape is { path, params, query, hash, name }. See
Dynamic Routes.
Namespace Types for unite()
unite() reads a variable from another file by namespace path.
The namespace codegen plugin emits a global declaration of that tree so the path completes and the return type is inferred.
// vite.config.ts
import { namespaceCodegenPlugin } from 'jafjs/vite'
export default defineConfig({
plugins: [
// ...the standard JAF plugins
namespaceCodegenPlugin(),
],
})
Namespaces follow the file path:
| File | Namespace |
|---|---|
pages/demo.html |
demo |
pages/admin/dashboard.html |
admin.dashboard |
components/Button.html |
Button |
components/ui/Modal.html |
ui.Modal |
<script>
// Inferred as the declared type of `theme` in pages/demo.html
let theme = unite(demo.theme)
</script>
See unite().
Component Prop Types
The component codegen scans your .html components and emits a registry of
their props:
// src/generated/components.d.ts (auto-generated)
declare module "jaf:components" {
export interface ComponentRegistry {
Button: { label?: string; disabled?: boolean }
Card: { title?: string }
}
export type ComponentName = keyof ComponentRegistry
export type ComponentProps<T extends ComponentName> = ComponentRegistry[T]
export type OptionalPropsComponent = /* components with no required props */
export type RequiredPropsComponent = Exclude<ComponentName, OptionalPropsComponent>
}
Not reachable in v0.1. componentTypesCodegenPlugin is
not re-exported from jafjs/vite, and the subpath its own documentation
names does not exist in the package. There is no supported import for it today, so
components.d.ts is not generated in any default setup. The generator
itself works; only the wiring is missing.
Because component tags are resolved by filename at build time rather than by import,
these types are for editor completion only. They do not check your templates - a typo in
a prop name inside <Button labl="x" /> is not a TypeScript error.
Types Inside Component Scripts
A component's <script> is compiled by SWC, and TypeScript annotations
are accepted and stripped:
<script>
interface User {
id: string
name: string
}
let users: User[] = []
let selected: User | null = null
function select(user: User) {
selected = user
}
</script>
These annotations are erased, not checked - tsc does not see inside
.html files.
Put logic you want type-checked in a .ts module and import it.
<script>
import { formatPrice } from '../lib/format' // fully type-checked
let total = formatPrice(1299)
</script>
What Needs an Import
Most of what you use in a component is injected and needs no import:
effect, computed, storage(),
url(), path(), unite(),
query(), mutation(), ui and the
lifecycle hooks.
The package entry is for the handful that are not:
import {
reactive, effect, computed, // outside a component
lazy,
setErrorHandler, handleError,
storage, store, persistedStore,
mountComponent, loadComponent,
} from 'jafjs'
The router is a subpath of its own, and none of it is global:
import { navigate, navigateTo, generatePath, guards } from 'jafjs/router'.
The API Reference lists every
subpath.
Related
- Configuration - the codegen plugin options
- Component Namespaces
- unite()