Working with TypeScript
Crank is written in TypeScript and provides excellent type safety out of the box. This guide covers the types and patterns you'll need to build type-safe Crank applications.
Typing Component Context
Generator components use this
to access the component context. In TypeScript's strict mode, you'll need to provide a type annotation for this
. Crank exports the Context
type for this purpose:
import {Context} from "@b9g/crank";
function *Timer(this: Context) {
let seconds = 0;
const interval = setInterval(() => this.refresh(() => seconds++), 1000);
for ({} of this) {
yield <div>Seconds: {seconds}</div>;
}
clearInterval(interval);
}
Typing Component Return Values
You’ll often want to add a return type to your components. Crank exports custom types to help you type the return types of components:
import {Element} from "@b9g/crank";
function SyncFn(): Element {
return <div>Hello world</div>;
}
function *SyncGen(): Generator<Element> {
for ({} of this) {
yield <div>Hello world</div>;
}
}
async function AsyncFn(): Promise<Element> {
return <div>Hello world</div>;
}
async function *AsyncGen(): AsyncGenerator<Element> {
for ({} of this) {
yield <div>Hello world</div>;
}
}
Element
is just the type returned by JSX expressions/createElement
. As you can see, you still have to modify the return type of functions based on whether the function is async or a generator. You can also use the type Child
which represents any valid value in an element tree.
function *SyncGen(): Generator<Child> {
yield true;
yield false;
yield null;
yield undefined;
yield 0;
yield 9001;
yield "Hello world";
yield <div>Hello world</div>;
}
Anything assignable to Child
can be part of the element tree, and almost anything can be assigned to Child
.
Typing Props
You can type the props object passed to components. This allows JSX elements which use your component as a tag to be type-checked.
function Greeting ({name}: {name: string}) {
return (
<div>Hello {name}</div>
);
}
const el = <Greeting name="Brian" />; // compiles
const el1 = <Greeting name={1} />; // throws a type error
ComponentProps Helper Type
Starting in Crank 0.7, you can use the ComponentProps
helper type to extract the props type from any component. This is useful when you need to reference a component's props in other type definitions or when creating higher-order components.
import {ComponentProps} from "@b9g/crank";
function Button({variant, children}: {variant: "primary" | "secondary", children: string}) {
return <button class={`btn btn-${variant}`}>{children}</button>;
}
// Extract Button's props type
type ButtonProps = ComponentProps<typeof Button>;
// Equivalent to: {variant: "primary" | "secondary", children: string}
// Use in higher-order components
function withLoading<T extends ComponentProps<any>>(Component: (props: T) => any) {
return function LoadingWrapper({loading, ...props}: T & {loading: boolean}) {
if (loading) return <div>Loading...</div>;
return <Component {...props} />;
};
}
const LoadingButton = withLoading(Button);
The children prop can be typed using the Children
type provided by Crank. The Children
type is a broad type which can be Child
or arbitrarily nested iterables of Child
. TypeScript doesn’t really provide a way to prevent functions from being used as the children
prop, but such patterns are strongly discouraged. You should typically treat children
as an opaque value only to be interpolated into JSX because its value can be almost anything.
import {Children} from "@b9g/crank";
function Greeting ({name, children}: {name: string, children: Children}) {
return (
<div>
Message for {name}: {children}
</div>
);
}
Typing Event Listeners
If you dispatch custom events, you may want parent event listeners to be typed with the event you bubbled automatically. To do so, you can use module augmentation to extend the EventMap
interface from the global Crank
module.
declare global {
module Crank {
interface EventMap {
"mybutton.click": CustomEvent<{id: string}>;
}
}
}
function MyButton (props) {
this.addEventListener("click", () => {
this.dispatchEvent(new CustomEvent("mybutton.click", {
bubbles: true,
detail: {id: props.id},
}));
});
return (
<button {...props} />
);
}
When importing the DOM or HTML renderers, the EventMap
will be extended with the GlobalEventHandlersMap
interface.
Typing Provisions
By default, calls to the context’s provide
and consume
methods will be loosely typed. If you want stricter typings of these methods, you can use module augmentation to extend the ProvisionMap
interface from the global Crank
module.
declare global {
module Crank {
interface ProvisionMap {
greeting: string;
}
}
}
function GreetingProvider(
{greeting, children}: {greeting: string, children?: unknown},
) {
this.provide("greeting", greeting);
return children;
}
function Greeting({name}: {name: string}) {
const greeting = this.consume("greeting");
return <p>{greeting}, {name}</p>;
}