TypeScript
Marko’s TypeScript support offers in-editor error checking, makes refactoring less scary, verifies that data matches expectations, and even helps with API design.
Enabling TypeScript in your Marko project
There are two (non-exclusive) ways to add TypeScript to a Marko project:
For sites and web apps, a
tsconfig.jsonfile at the project root is the only requirement:src/ package.json tsconfig.jsonFor packages of Marko tags, the
"script-lang"attribute must be set to"ts"in themarko.json:marko.json{ "script-lang": "ts" }This will automatically expose type-checking and autocomplete for the published tags.
You can also use the script-lang method for sites and apps.
Marko will crawl up the directory looking for a marko.json with script-lang defined.
This helps when incrementally migrating to TypeScript allowing folders to opt-in or opt-out of strict type checking.
Typing 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
<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>
</label>label
-- Price in ${input.currency}:${" "}
input type="number" value=input.amount min=0 step=0.01export interface Input {
currency: string;
amount: number;
}
label
-- Price in ${input.currency}:${" "}
input type="number" value=input.amount min=0 step=0.01Since 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";import { Input as PriceInput } from "<PriceField>";
import { ExtraTypes } from "lib/utils.ts";
export type Input = PriceInput & ExtraTypesimport { Input as PriceInput } from "<PriceField>";
import { ExtraTypes } from "lib/utils.ts";import { Input as PriceInput } from "<PriceField>";
import { ExtraTypes } from "lib/utils.ts";
export type Input = PriceInput & ExtraTypesimport { Input as PriceInput } from "<PriceField>";import { Input as PriceInput } from "<PriceField>";
export interface Input extends PriceInput {
discounted: boolean;
expiresAt: Date;
}import { Input as PriceInput } from "<PriceField>";import { Input as PriceInput } from "<PriceField>";
export interface Input extends PriceInput {
discounted: boolean;
expiresAt: Date;
}Generic Input
Generic Types and Type Parameters on Input are recognized throughout the entire .marko template (excluding static statements).
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>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>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}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}Built-in Marko Types
Marko exposes common type definitions through the Marko TypeScript namespace:
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
Inputand$globalvalues.
- The object accepted by the render methods of a template. It includes the template's
Marko.Body<Params, Return>- Used to type tag content
Marko.Renderable- All values accepted by the
<${dynamic}/>tag string | Marko.Template | Marko.Body | { content: Marko.Body}
- All values accepted by the
Marko.Global- The type of the
$globalobject
- The type of the
Marko.RenderResult- The result of rendering a Marko template
ReturnType<template.renderSync>Awaited<ReturnType<template.render>>
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 from native tags (when a string is passed) or custom tags.
Marko.BodyParameters<Body>andMarko.BodyReturnType<Body>- Helper to extract the parameters and return types from a
Marko.Body
- Helper to extract the parameters and return types from a
Marko.AttrTag<T>- Used to represent types for attributes tags
- A single attribute tag, with a
[Symbol.iterator]to consume any repeated tags
Deprecated
Marko.Component<Input, State>- The base class for a class component
Marko.Out- The render context with methods like
write,beginAsync, etc. ReturnType<template.render>
- The render context with methods like
Marko.EmitterEventEmitterfrom@types/node
Typing content
A commonly used type from the Marko namespace is Marko.Body which can be used to type the content in input.content:
export interface Input {
content?: Marko.Body;
}Here, all of the following are acceptable:
<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 componentsPassing other values (including components) causes a type error:
import OtherTag from "<other-tag>";
<child content=OtherTag/>import OtherTag from "<other-tag>";
child content=OtherTagTyping Tag Parameters
Tag parameters are provided to the content by the child tag. For this reason, Marko.Body allows typing of its parameters:
<for|i| from=0 to=input.to by=2>
<${input.content}(i)/>
</for>export interface Input {
to: number;
content: Marko.Body<[number]>
}
<for|i| from=0 to=input.to by=2>
<${input.content}(i)/>
</for>for|i| from=0 to=input.to by=2
${input.content}(i)export interface Input {
to: number;
content: Marko.Body<[number]>
}
for|i| from=0 to=input.to by=2
${input.content}(i)<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:
$ const { color, ...attrs } = input
<button style=`color: ${color}` ...attrs>
<content/>
</button>export interface Input extends Marko.Input<"button"> {
color: string;
content?: Marko.Body;
}
$ const { color, ...attrs } = input
<button style=`color: ${color}` ...attrs>
<content/>
</button>$ const { color, ...attrs } = input
button style=`color: ${color}` ...attrs
contentexport interface Input extends Marko.Input<"button"> {
color: string;
content?: Marko.Body;
}
$ const { color, ...attrs } = input
button style=`color: ${color}` ...attrs
contentRegistering a new native tag (e.g. 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.
<my-tag foo=1>
${(input.el as HTMLInputElement).value}
</my-tag><my-tag foo=1 as any>
${(input.el as HTMLInputElement).value}
</my-tag>my-tag foo=1 -- ${(input.el as HTMLInputElement).value}my-tag foo=1 as any -- ${(input.el as HTMLInputElement).value}Tag Type Parameters
<child <T>|value: T|>
...
</child>child <T>|value: T| -- ...Tag Type Arguments
export interface Input<T> {
value: T;
}// 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=1Method 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 names=[]/><some-component number=1 as const names=([] as string[])/>some-component number=1 names=[]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-checkIf 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}CI Type Checking
For type checking Marko files outside of your editor there is the @marko/type-check cli. See the CLI documentation for more information.
Profiling Performance
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_DIRContributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.