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.
Tip
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
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
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>";
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 Input
Generic 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>
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;
}
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:
export interface Input {
to: number;
content: Marko.Body<[number]>;
}
<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-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:
export interface Input extends Marko.Input<"button"> {
color: string;
content?: Marko.Body;
}
$ 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
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 as any>
${(input.el as HTMLInputElement).value}
</my-tag>
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
/* components/child.marko */
export interface Input<T> {
value: T;
}
/* components/child.marko */
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 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}
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.
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.