embedded-typescript

Type safe embedded TypeScript templates

Usage no npm install needed!

<script type="module">
  import embeddedTypescript from 'https://cdn.skypack.dev/embedded-typescript';
</script>

README

Embedded TypeScript

Type safe embedded TypeScript templates

What is this? 🧐

A type safe templating system for TypeScript. Templates are compiled to TypeScript files that are then imported for type safe string generation.

This templating system draws inspiration from ERB, EJS, handlebars and mustache. This project embraces the "just JavaScript" spirit of ejs and adds some of the helpful white space semantics of mustache.

Checkout the examples or play with embedded-typescript in your browser.

Installation & Usage 📦

  1. Add this package to your project:
    • yarn add embedded-typescript

Motivation

Hello undefined!

When using a typed language, I want my templates to be type checked. For most cases, template literals work well. If I'm writing HTML/XML, JSX works well. When I have complicated non-HTML/XML templates, template literals become difficult to maintain. I can use ejs/hbs/mustache/etc, but then I lose the type safety I had with template literals. Sometimes I want the expressiveness of a templating language without losing type safety. For those cases, I wrote embedded-typescript.

Syntax

<%> — Begin or end a templated region. The syntax below is only valid inside a templated region.

<%= EXPRESSION %> — Inserts the value of an expression. If the expression generates multiple lines, the indentation level is preserved across all resulting lines.

<% CODE %> — Executes code, but does not insert a value.

TEXT - Text literals are inserted as is. All white space is preserved.

Examples 🚀

Minimal

  1. Write a template file: my-template.ets:
interface User {
  name: string;
}

export function render(users: User[]): string {
  return <%>
    <% users.forEach(function(user) { %>
Name: <%= user.name %>
    <% }) %>
  <%>
}
  1. Run the compiler: yarn ets. This will compile any files with the .ets extension. my-template.ets.ts will be generated.

  2. Import from the generated .ets.ts file:

import { render } from "./my-template.ets";

/* will output:
Name: Alice
Name: Bob
*/

console.log(render([{ name: "Alice" }, { name: "Bob" }]));

Partials

  1. Optionally write a "partial" user-partial.ets:
export interface User {
  name: string;
  email: string;
  phone: string;
}

export function render(user: User): string {
  return <%>
Name: <%= user.name %>
Email: <%= user.email %>
Phone: <%= user.phone %>
  <%>
}
  1. Import your "partial" in my-template-2.ets:
import { render as renderUser, User } from './user-partial.ets';

const example =
`1
2
3
4`;

export function render(users: User[]): string {
  return <%>
<% if (users.length > 0) { %>
Here is a list of users:
  <% users.forEach(function(user) { %>

  <%= renderUser(user) %>
  <% }) %>

<% } %>
The indentation level is preserved for the rendered 'partial'.

There isn't anything special about the 'partial'. Here we used another ets template, but any
expression yeilding a multiline string would be treated the same.

  <%= example %>

The end!
  <%>
}
  1. Import your template:
import { render } from "./my-template-1.ets";

/* will output:
Here is a list of users:

  Name: Tate
  Email: tate@tate.com
  Phone: 888-888-8888

  Name: Emily
  Email: emily@emily.com
  Phone: 777-777-7777

The indentation level is preserved for the rendered 'partial'.

There isn't anything special about the 'partial'. Here we used another ets template, but any
expression yeilding a multiline string would be treated the same.

  1
  2
  3
  4

The end!
*/

console.log(
  render([
    { name: "Tate", phone: "888-888-8888", email: "tate@tate.com" },
    { name: "Emily", phone: "777-777-7777", email: "emily@emily.com" },
  ])
);

Note that the arguments to your template function are typechecked. There isn't anything special about the render export, in our template file this could have been named anything: printUserNames.

For more examples, take a look at the example directory. The *.ets.ts files are generated by the compiler from the *.ets template files. The corresponding *${NAME}.test.ts shows example usage and output.

Understanding Error Messages

The compiler will output errors when it encounters invalid syntax:

error: Unexpected closing tag '%>'
   --> ./template-1.ets:4:41
    |
4   | <% users.forEach(function(user) { %>%>
    |                                     ^
    |                                     |
...

The first line is a description of the error that was encountered.

The second line is location of the error, in path:line:column notation.

The next 5 lines provide visual context for the error.

Notable deviations from prior art

Rather than treating the entire contents of an input file as templated text, embedded-typescript templates explicitly indicate templated regions (delimited by<%>). This symbol is used to both start and end a templated region. Outside of a templating region file contents are treated as TypeScript. This enables complete control over the namespace: you explicitly specify exports and their type signature. Because it's just TypeScript, outside of a templated region, you can import any helpers, utilities, partials, etc. If you've worked with JSX things should feel familiar.

This tool specifically targets text templating, rather than HTML templating. Think: code generation, text message content (emails or SMS), etc. HTML templating is possible with this tool, but I would generally recommend JSX instead for HTML cases.

The templating system does not perform any HTML escaping. You can import any self authored or 3rd party HTML escaping utilities in your template, and call that directly on any untrusted input:

import htmlescape from 'htmlescape';

interface User {
  name: string;
}

export function render(users: User[]): string {
  return <%>
    <% users.forEach(function(user) { %>
<p>Name: <%= htmlescape(user.name) %></p>
    <% }) %>
  <%>
}

Highlights

🎁 Zero run time dependencies

Contributing 👫

PR's and issues welcomed! For more guidance check out CONTRIBUTING.md

Licensing 📃

See the project's MIT License.