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.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 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.01
export interface Input {
currency: string;
amount: number;
}
label
-- Price in ${input.currency}:${" "}
input type="number" value=input.amount min=0 step=0.01
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";
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";
import { Input as PriceInput } from "<PriceField>";
import { ExtraTypes } from "lib/utils.ts";
export type Input = PriceInput & ExtraTypes
import { 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
.marko
file 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
Input
and$global
values.
- 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
$global
object
- The type of the
Marko.RenderResult
- The result of rendering a Marko template
ReturnType<template.renderSync>
Awaited<ReturnType<template.render>>
Marko.NativeTags
Marko.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.Emitter
EventEmitter
from@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 components
Passing other values (including components) causes a type error:
import OtherTag from "<other-tag>";
<child content=OtherTag/>
import OtherTag from "<other-tag>";
child content=OtherTag
Typing 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
content
export interface Input extends Marko.Input<"button"> {
color: string;
content?: Marko.Body;
}
$ const { color, ...attrs } = input
button style=`color: ${color}` ...attrs
content
Registering 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=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 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-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}
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_DIR
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.