docs/reference/typescript.md
Marko’s TypeScript support offers in-editor error checking, makes refactoring less scary, verifies that data matches expectations, and even helps with API design.
There are two (non-exclusive) ways to add TypeScript to a Marko project:
For sites and web apps, a tsconfig.json file at the project root is the only requirement:
src/
package.json
tsconfig.json
For packages of Marko tags, the "script-lang" attribute must be set to "ts" in the marko.json:
/* marko.json */
{
"script-lang": "ts"
}
This will automatically expose type-checking and autocomplete for the published tags.
[!TIP] You can also use the
script-langmethod for sites and apps.Marko will crawl up the directory looking for a
marko.jsonwithscript-langdefined.This helps when incrementally migrating to TypeScript allowing folders to opt-in or opt-out of strict type checking.
inputA .marko file will use any exported Input type for that file’s input object.
This can be export type Input or export interface Input.
/* 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>
Since it is exported, Input may be accessed from other .marko and .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>";
export interface Input extends PriceInput {
discounted: boolean;
expiresAt: Date;
};
InputGeneric Types and Type Parameters on Input are recognized throughout the entire .marko template (excluding static statements).
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 onInput(evt) { input.onSelect(options[evt.target.value] as T) }>
<for|value, i| of=input.options>
<option value=i>${value}</option>
</for>
</select>
Marko exposes common type definitions through the Marko TypeScript namespace:
Marko.Template<Input, Return>
.marko filetypeof import("./template.marko")Marko.TemplateInput<Input>
Input and $global values.Marko.Body<Params, Return>
Marko.Renderable
<${dynamic}/> tagstring | Marko.Template | Marko.Body | { content: Marko.Body}Marko.Global
$global objectMarko.RenderResult
ReturnType<template.renderSync>Awaited<ReturnType<template.render>>Marko.NativeTags
Marko.NativeTags: An object containing all native tags and their typesMarko.Input<TagName> and Marko.Return<TagName>
Marko.BodyParameters<Body> and Marko.BodyReturnType<Body>
Marko.BodyMarko.AttrTag<T>
[Symbol.iterator] to consume any repeated tagsMarko.Component<Input, State>
Marko.Out
write, beginAsync, etc.ReturnType<template.render>Marko.Emitter
EventEmitter from @types/nodecontentA commonly used type from the Marko namespace is Marko.Body which can be used to type the content in input.content:
/* child.marko */
export interface Input {
content?: Marko.Body;
}
Here, all of the following are acceptable:
/* index.marko */
<child/>
<child>Text in render body</child>
<child>
<div>Any combination of components</div>
</child>
Passing other values (including components) causes a type error:
/* index.marko */
import OtherTag from "<other-tag>";
<child content=OtherTag/>
Tag parameters are provided to the content by the child tag. For this reason, Marko.Body allows typing of its parameters:
/* for-by-two.marko */
export interface Input {
to: number;
content: Marko.Body<[number]>
}
<for|i| from=0 to=input.to by=2>
<${input.content}(i)/>
</for>
/* index.marko */
<for-by-two|i| to=10>
<div>${i}</div>
</for-by-two>
All attribute tags are typed as iterable with a [Symbol.iterator], regardless of intent. This means all attribute tag inputs must be wrapped in Marko.AttrTag.
/* my-select.marko */
export interface Input {
option: Marko.AttrTag<Marko.HTML.Option>
}
<select>
<for|option| of=input.option>
<option ...option/>
</for>
</select>
The types for native tags are accessed via the global Marko.HTML namespace. Here's an example of a component that extends the button html tag:
/* color-button.marko */
export interface Input extends Marko.HTML.Button {
color: string;
}
<const/{ color, ...attrs }=input>
<button style=`color: ${color}` ...attrs/>
[!TIP] Since Marko 6, native tags have supported including
contentas an attribute so there is no need to inject manuallymarko<button style=`color: ${color}` ...attrs> // no longer required! <${input.content}/> </button>
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;
}
}
}
declare global {
namespace Marko {
interface HTMLAttributes {
"my-non-standard-attribute"?: string; // Adds this attribute as available on all HTML tags.
}
}
}
declare global {
namespace Marko {
namespace CSS {
interface Properties {
"--foo"?: string; // adds a support for a custom `--foo` css property.
}
}
}
}
.markoAny JavaScript expression in Marko can also be written as a TypeScript expression.
<my-tag foo=1 as any>
${(input.el as HTMLInputElement).value}
</my-tag>
<child <T>|value: T|>
...
</child>
/* components/child.marko */
export interface Input<T> {
value: T;
}
/* index.marko */
// number would be inferred in this case, but we can be explicit
<child<number> value=1 />
<child process<T>() { /* ... */ } />
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[]
/>
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.
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>
For type checking Marko files outside of your editor there is the @marko/type-check cli. See the CLI documentation for more information.
The --generateTrace flag can be used to determine the parts of a codebase which are using the most resources during type checking.
mtc --generateTrace TRACE_DIR