marko
Version:
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
407 lines (305 loc) • 10.8 kB
Markdown
# 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.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) at the project root:
<pre>
📁 components/
📁 node_modules/
<img src="./icons/marko.svg" width=16> index.marko
📦 package.json
<mark><img src="./icons/ts.svg" width=16> tsconfig.json</mark>
</pre>
- **If you’re [publishing packages of Marko tags](https://markojs.com/docs/custom-tags/#publishing-tags-to-npm)**, add the following to [your `marko.json`](./marko-json.md):
```json
"script-lang": "ts"
```
This will automatically expose type-checking and autocomplete for the published tags.
> **ProTip**: You can also use the `script-lang` method for sites and apps.
## Typing a tag's `input`
A `.marko` file will use any exported `Input` type for [that file’s `input` object](./class-components.md#input).
This can be `export type Input` or `export interface Input`.
### Example
_PriceField.marko_
```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>
```
You can also import, reuse, and extend `Input` interfaces from other `.marko` or `.ts` files:
```marko
import { Input as PriceInput } from "<PriceField>";
import { ExtraTypes } from "lib/utils.ts";
export type Input = PriceInput & ExtraTypes;
```
```marko
import { Input as PriceInput } from "<PriceField>";
export interface Input extends PriceInput {
discounted: boolean;
expiresAt: Date;
};
```
### Generic `Input`s
[Generic Types and Type Parameters](https://www.typescriptlang.org/docs/handbook/2/generics.html) on `Input` are recognized throughout the entire `.marko` template (excluding [static statements](./syntax.md#static-javascript)).
For example, if you set up a component like this:
_components/my-select.marko_
```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>
```
…then your editor will figure out the types of inputs to that component:
```marko
<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](https://github.com/marko-js/marko/blob/main/packages/runtime-class/index.d.ts) you can reuse in [a TypeScript namespace](https://www.typescriptlang.org/docs/handbook/namespaces.html) called `Marko`:
- **`Marko.Template<Input, Return>`**
- The type of a `.marko` file
- `typeof import("./template.marko")`
- **`Marko.TemplateInput<Input>`**
- The object accepted by the render methods of a template. It includes the template's `Input` as well as `$global` values.
- **`Marko.Body<Params, Return>`**
- The type of the [body content](./body-content.md) of a tag (`renderBody`)
- **`Marko.Component<Input, State>`**
- The base class for a [class component](./class-components.md)
- **`Marko.Renderable`**
- Values accepted by the [`<${dynamic}/>` tag](./syntax.md#dynamic-tagname)
- `string | Marko.Template | Marko.Body | { renderBody: Marko.Body}`
- **`Marko.Out`**
- The render context with methods like `write`, `beginAsync`, etc.
- `ReturnType<template.render>`
- **`Marko.Global`**
- The type of the object in `$global` and `out.global` that can be passed to a template's render methods as the `$global` property.
- **`Marko.RenderResult`**
- The [result](./rendering.md#renderresult) of rendering a Marko template
- `ReturnType<template.renderSync>`
- `Awaited<ReturnType<template.render>>`
- **`Marko.Emitter`**
- `EventEmitter` from `@types/node`
- **`Marko.NativeTags`**
- `Marko.NativeTags`: An object containing all native tags and their types
- **`Marko.Input<TagName>`** and **`Marko.Return<TagName>`**
- Helpers to extract the input and return types native tags (when a string is passed) or a custom tag.
- **`Marko.BodyParameters<Body>`** and **`Marko.BodyReturnType<Body>`**
- Helpers to extract the parameters and return types from the specified `Marko.Body`
- **`Marko.AttrTag<T>`**
- Used to represent types for [attributes tags](./body-content.md#named-body-content)
- 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_
```marko
export interface Input {
renderBody?: Marko.Body;
}
```
Here, the following will be acceptable values:
_index.marko_
```marko
<child/>
<child>Text in render body</child>
<child>
<div>Any combination of components</div>
</child>
```
Passing other values (including components) will cause a type error:
_index.marko_
```marko
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_
```marko
export interface Input {
to: number;
renderBody: Marko.Body<[number]>
}
<for|i| from=0 to=input.to by=2>
<${input.renderBody}(i)/>
</for>
```
_index.marko_
```marko
<for-by-two|i| to=10>
<div>${i}</div>
</for-by-two>
```
### 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_
```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>
```
### Registering a new native tag (eg for custom elements).
```ts
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
```ts
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)
```ts
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](./syntax.md#inline-javascript) can also be written as a TypeScript expression.
### Tag Type Parameters
```marko
<child <T>|value: T|>
...
</child>
```
### Tag Type Arguments
_components/child.marko_
```marko
export interface Input<T> {
value: T;
}
```
_index.marko_
```marko
// number would be inferred in this case, but we can be explicit
<child<number> value=1 />
```
### Method Shorthand Type Parameters
```marko
<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](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions):
```marko
<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](https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html).
## Setup
You can enable type checking in an existing `.marko` file by adding a `// @ts-check` comment at the top:
```js
// @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`](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#using-tsconfigjson-or-jsconfigjson). 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`:
```marko
// @ts-check
/**
* @typedef {{
* firstName: string,
* lastName: string,
* }} Input
*/
<div>${firstName} ${lastName}</div>
```
## With a separate `component.js` file
Many components in existing projects adhere to the following structure:
<pre>
📁 components/
📁 color-rotate-button/
<img src="./icons/marko.svg" width=16> index.marko
<img src="./icons/js.svg" width=16> component.js
</pre>
The `color-rotate-button` takes a list of colors and moves to the next one each time the button is clicked:
```marko
<color-rotate-button colors=["red", "blue", "yellow"]>
Next Color
</color-rotate-button>
```
Here is an example of how this `color-rotate-button` component could be typed:
_components/color-rotate-button/component.js_
```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_
```marko
// @ts-check
/* Input will be automatically imported from `component.js`! */
<button
onClick('rotateColor')
style=`color: ${input.colors[state.colorIndex]}`>
<${input.renderBody}/>
</button>
```
# CI Type Checking
For type checking Marko files outside of your editor there is the ["@marko/type-check" cli](https://github.com/marko-js/language-server/tree/main/packages/type-check).
Check out the CLI documentation for more information.