
TypeScript in Marko
Note: Types are supported in Marko v5.22.7+ and Marko v4.24.6+
Marko’s TypeScript support offers in-editor error checking, makes refactoring less scary, verifies that data matches expectations, and even helps with API design.
Or maybe you just want more autocomplete in VSCode. That works too.
Enabling TypeScript in your Marko project
There are two (non-exclusive) ways to add TypeScript to a Marko project:
- For sites and web apps, you can place a
tsconfig.jsonfile at the project root:📁 components/ 📁 node_modules/
index.marko 📦 package.json
tsconfig.json
- If you’re publishing packages of Marko tags, add the following to your
marko.json:"script-lang": "ts"
This will automatically expose type-checking and autocomplete for the published tags.
ProTip: You can also use the
script-langmethod for sites and apps.
Typing a tag's input
A .marko file will use any exported Input type for that file’s input object.
This can be export type Input or export interface Input.
Example
PriceField.marko
export interface Input { currency: string; amount: number; } <label> Price in ${input.currency}: <input type="number" value=input.amount min=0 step=0.01> </label>
export interface Input { currency: string; amount: number; } label -- Price in ${input.currency}:${" "} input type="number" value=input.amount min=0 step=0.01
You can also import, reuse, and extend Input interfaces from other .marko or .ts files:
import { Input as PriceInput } from "<PriceField>"; import { ExtraTypes } from "lib/utils.ts"; export type Input = PriceInput & ExtraTypes;
import { Input as PriceInput } from "<PriceField>"; import { ExtraTypes } from "lib/utils.ts"; export type Input = PriceInput & ExtraTypes;
import { Input as PriceInput } from "<PriceField>"; export interface Input extends PriceInput { discounted: boolean; expiresAt: Date; };
import { Input as PriceInput } from "<PriceField>"; export interface Input extends PriceInput { discounted: boolean; expiresAt: Date; }
Generic Inputs
Generic Types and Type Parameters on Input are recognized throughout the entire .marko template (excluding static statements).
For example, if you set up a component like this:
components/my-select.marko
export interface Input<T> { options: T[]; onSelect: (newVal: T) => unknown; } static function staticFn() { // can NOT use `T` here } $ const instanceFn = (val: T) => { // can use `T` here } // can use `as T` here <select on-input(evt => input.onSelect(options[evt.target.value] as T))> <for|value, i| of=input.options> <option value=i>${value}</option> </for> </select>
export interface Input<T> { options: T[]; onSelect: (newVal: T) => unknown; } static function staticFn() { // can NOT use `T` here } $ const instanceFn = (val: T) => { // can use `T` here }; // can use `as T` here select on-input((evt) => input.onSelect(options[evt.target.value] as T)) for|value, i| of=input.options option value=i -- ${value}
…then your editor will figure out the types of inputs to that component:
<my-select options=[1,2,3] onSelect=val => {}/> // ^^^ number <my-select options=["M","K","O"] onSelect=val => {}/> // ^^^ string
my-select options=[1, 2, 3] onSelect=(val) => {} // ^^^ number my-select options=["M", "K", "O"] onSelect=(val) => {} // ^^^ string
Built-in Marko Types
Marko exposes type definitions you can reuse in a TypeScript namespace called Marko:
Marko.Template<Input, Return>- The type of a
.markofile typeof import("./template.marko")
- The type of a
Marko.TemplateInput<Input>- The object accepted by the render methods of a template. It includes the template's
Inputas well as$globalvalues.
- The object accepted by the render methods of a template. It includes the template's
Marko.Body<Params, Return>- The type of the body content of a tag (
renderBody)
- The type of the body content of a tag (
Marko.Component<Input, State>- The base class for a class component
Marko.Renderable- Values accepted by the
<${dynamic}/>tag string | Marko.Template | Marko.Body | { renderBody: Marko.Body}
- Values accepted by the
Marko.Out- The render context with methods like
write,beginAsync, etc. ReturnType<template.render>
- The render context with methods like
Marko.Global- The type of the object in
$globalandout.globalthat can be passed to a template's render methods as the$globalproperty.
- The type of the object in
Marko.RenderResult- The result of rendering a Marko template
ReturnType<template.renderSync>Awaited<ReturnType<template.render>>
Marko.EmitterEventEmitterfrom@types/node
Marko.NativeTagsMarko.NativeTags: An object containing all native tags and their types
Marko.Input<TagName>andMarko.Return<TagName>- Helpers to extract the input and return types native tags (when a string is passed) or a custom tag.
Marko.BodyParameters<Body>andMarko.BodyReturnType<Body>- Helpers to extract the parameters and return types from the specified
Marko.Body
- Helpers to extract the parameters and return types from the specified
Marko.AttrTag<T>- Used to represent types for attributes tags
- A single attribute tag, with a
[Symbol.iterator]to consume any repeated tags.
Typing renderBody
The most commonly used type from the Marko namespace is Marko.Body which can be used to type input.renderBody:
child.marko
export interface Input { renderBody?: Marko.Body; }
export interface Input { renderBody?: Marko.Body; }
Here, the following will be acceptable values:
index.marko
<child/> <child>Text in render body</child> <child> <div>Any combination of components</div> </child>
child child -- Text in render body child div -- Any combination of components
Passing other values (including components) will cause a type error:
index.marko
import OtherTag from "<other-tag>"; <child renderBody=OtherTag/>
import OtherTag from "<other-tag>"; child renderBody=OtherTag
Typing Tag Parameters
Tag parameters are passed to the renderBody by the child tag. For this reason, Marko.Body also allows typing of its parameters:
for-by-two.marko
export interface Input { to: number; renderBody: Marko.Body<[number]> } <for|i| from=0 to=input.to by=2> <${input.renderBody}(i)/> </for>
export interface Input { to: number; renderBody: Marko.Body<[number]>; } for|i| from=0 to=input.to by=2 ${input.renderBody}(i)
index.marko
<for-by-two|i| to=10> <div>${i}</div> </for-by-two>
for-by-two|i| to=10 div -- ${i}
Extending native tag types within a Marko tag
The types for native tags are accessed via the global Marko.Input type. Here's an example of a component that extends the button html tag:
color-button.marko
export interface Input extends Marko.Input<"button"> { color: string; renderBody?: Marko.Body; } $ const { color, renderBody, ...restOfInput } = input; <button style=`color: ${color}` ...restOfInput> <${renderBody}/> </button>
export interface Input extends Marko.Input<"button"> { color: string; renderBody?: Marko.Body; } $ const { color, renderBody, ...restOfInput } = input; button style=`color: ${color}` ...restOfInput ${renderBody}
Registering a new native tag (eg for custom elements).
interface MyCustomElementAttributes { // ... } declare global { namespace Marko { interface NativeTags { // By adding this entry, you can now use `my-custom-element` as a native html tag. "my-custom-element": MyCustomElementAttributes; } } }
Registering new "global" HTML Attributes
declare global { namespace Marko { interface HTMLAttributes { "my-non-standard-attribute"?: string; // Adds this attribute as available on all HTML tags. } } }
Registering CSS Properties (eg for custom properties)
declare global { namespace Marko { namespace CSS { interface Properties { "--foo"?: string; // adds a support for a custom `--foo` css property. } } } }
TypeScript Syntax in .marko
Any JavaScript expression in Marko can also be written as a TypeScript expression.
Tag Type Parameters
<child <T>|value: T|> ... </child>
child <T>|value: T| -- ...
Tag Type Arguments
components/child.marko
export interface Input<T> { value: T; }
export interface Input<T> { value: T; }
index.marko
// number would be inferred in this case, but we can be explicit <child<number> value=1 />
// number would be inferred in this case, but we can be explicit child<number> value=1
Method Shorthand Type Parameters
<child process<T>() { /* ... */ } />
child process<T>() { /* ... */ }
Attribute Type Assertions
The types of attribute values can usually be inferred. When needed, you can assert values to be more specific with TypeScript’s as keyword:
<some-component number=1 as const names=[] as string[] />
some-component number=1 as const names=([] as string[])
JSDoc Support
For existing projects that want to incrementally add type safety, adding full TypeScript support is a big leap. This is why Marko also includes full support for incremental typing via JSDoc.
Setup
You can enable type checking in an existing .marko file by adding a // @ts-check comment at the top:
// @ts-check
If you want to enable type checking for all Marko & JavaScript files in a JavaScript project, you can switch to using a jsconfig.json. You can skip checking some files by adding a // @ts-nocheck comment to files.
Once that has been enabled, you can start by typing the input with JSDoc. Here's an example component with typed input:
// @ts-check /** * @typedef {{ * firstName: string, * lastName: string, * }} Input */ <div>${firstName} ${lastName}</div>
// @ts-check /** * @typedef {{ * firstName: string, * lastName: string, * }} Input */ div -- ${firstName} ${lastName}
With a separate component.js file
Many components in existing projects adhere to the following structure:
📁 components/
📁 color-rotate-button/
index.marko
component.js
The color-rotate-button takes a list of colors and moves to the next one each time the button is clicked:
<color-rotate-button colors=["red", "blue", "yellow"]> Next Color </color-rotate-button>
color-rotate-button colors=["red", "blue", "yellow"] -- Next Color
Here is an example of how this color-rotate-button component could be typed:
components/color-rotate-button/component.js
// @ts-check /** * @typedef {{ * colors: string[], * renderBody: Marko.Renderable * }} Input * @typedef {{ * colorIndex: number * }} State * @extends {Marko.Component<Input, State>} */ export default class extends Marko.Component { onCreate() { this.state = { colorIndex: 0, }; } rotateColor() { this.state.colorIndex = (this.state.colorIndex + 1) % this.input.colors.length; } }
components/color-rotate-button/index.marko
// @ts-check /* Input will be automatically imported from `component.js`! */ <button onClick('rotateColor') style=`color: ${input.colors[state.colorIndex]}`> <${input.renderBody}/> </button>
// @ts-check /* Input will be automatically imported from `component.js`! */ button onClick("rotateColor") style=`color: ${input.colors[state.colorIndex]}` ${input.renderBody}
CI Type Checking
For type checking Marko files outside of your editor there is the "@marko/type-check" cli. Check out the CLI documentation for more information.
EDITContributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.