npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typed-html-templates

v1.0.3

Published

HTM-compatible html tagged templates with strong TypeScript hole typing and DOM-aware inference.

Readme

typed-html-templates

Type-safe HTM templates on top of htm.

This package keeps HTM runtime semantics (htm.bind(h)) and adds compile-time type-safety for interpolation holes (${...}) using a type-level parser/state machine.

Why this exists

Ryan Carniato described the problem well:

"I've been looking at html tagged template literals again and man the state of things sucks. I'd be so happy if @_developit's htm was the accepted format and we could just get typing going, but as usual there are competing standards and in a lot of ways it makes sense.

  1. We have WC folks who have a strong voice in standards that don't care about anything beyond WCs (why should they) but unfortunately WCs are severely limited in a universal sense, so can never be the actual complete answer.

  2. We have TS that doesn't want to go through implementing a specific thing like JSX again so unless standards forces their hands they probably aren't going to move.

  3. The options for representing tags are either untypable slots, inline case sensitive (goes against standard case insensitivity), or bulky beyond reasonable usage(call create element yourself).

I would appeal to WC folks to support some exotic syntax. But honestly if I were them I wouldn't want to. But then again they have no issue proposing their own exotic syntax (like ., @, and ?) which IMHO should only be included if HTML supports them. So I get this does devolve into people trying to standardize their own framework. But the TS need is real. A real conundrum."

This package aims to be that "get typing going" layer for HTM-compatible templates.

Install

npm install typed-html-templates

Quick start

import html from 'typed-html-templates';

const node = html`<div id=${'hello'}>${'world'}</div>`;

html returns Node | Node[] because HTM supports multiple root nodes. If you want a strict single-root result, use single(...) or bindSingle(h):

import html, { single } from 'typed-html-templates';

const node = single(html`<div id=${'hello'}>${'world'}</div>`);

You can also use the template(parts, ...values) helper when you want explicit tuples that preserve inference:

import html, { template } from 'typed-html-templates';

const result = html(...template(['<input value=', ' checked=', ' />'] as const, 'ok', true));

If you prefer not to write as const at call sites, use asConst(...):

import html, { asConst, template } from 'typed-html-templates';

const result = html(...template(asConst(['<input value=', ' />']), 'ok'));

Aliases are included for readability in different contexts:

  • template.from(...)
  • template.dom(...)
  • template.one(...)

You can also use html.params(...) / html.from(...) (and strict equivalents) for a plain variadic-strings style:

const renderInput = html.params('<input value=', ' />');
const node = renderInput('ok');

params/from are primarily for root-shape ergonomics and readability. For strongest hole-level type safety, prefer template(...) + spread into html(...).

For editor DX, template(...) usually gives the strongest root-tag inference (for example, input -> HTMLInputElement).

For static intrinsic roots, the default node type also carries a typed element? field:

  • <div ...> -> HTMLDivElement | undefined
  • <input ...> -> HTMLInputElement | undefined

Safety guarantees

When template parts are statically known (tagged templates and template(...) helper), the library enforces:

  • hole-kind checking (tag vs attr-value vs spread vs child)
  • intrinsic attr typing (DOM map by default)
  • dynamic component prop typing (<${Comp} ...>)
  • spread shape checking (...${props})
  • event handler typing for on* attrs

This means invalid hole values fail at compile time (via TypeScript), not just at runtime.

Strict mode

Use bindStrict(h) when you want unknown attrs to fail for known intrinsic tags and components:

import { bindStrict, template } from 'typed-html-templates';

const h = (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => ({
  type,
  props,
  children
});

const html = bindStrict(h);

html`<input value=${'ok'} />`; // ok
// @ts-expect-error unknown intrinsic attr
html(...template(['<input notARealProp=', ' />'] as const, 'x'));

const Card = (_props: { title: string }) => null;
// @ts-expect-error unknown component prop
html(...template(['<', ' nope=', ' />'] as const, Card, 'x'));

If you want strict mode with the default binding, use strictHtml directly:

import { strictHtml, template } from 'typed-html-templates';

strictHtml`<input value=${'ok'} />`;
// @ts-expect-error unknown intrinsic attr in strict mode
strictHtml(...template.one(['<input notARealProp=', ' />'] as const, 'x'));

Inference notes

There are two kinds of inference in play:

  1. Hole type inference (what each ${...} must be)
  2. Root node narrowing (whether result becomes Node<"input", ...> vs generic Node<string, ...>)

Hole type inference is strong in both styles:

  • html tagged template form
  • html(...template(parts, ...values)) tuple helper form

Root node narrowing is strongest with template(...) tuple form:

const node = single(html(...template(['<input value=', ' />'] as const, 'ok')));
// node.type -> "input"
// node.element -> HTMLInputElement | undefined

In plain tagged-template syntax, TypeScript may widen root tag information depending on compiler inference limits:

const node = single(html`<input value=${'ok'} />`);
// node.type may be `string`
// node.element may be `HTMLElement | undefined`

This is a TypeScript inference limitation around tagged-template literal preservation, not a runtime behavior difference.

The default export is bind(defaultH), where defaultH returns:

{ type, props, children }

Use with your renderer

Like HTM, this package is renderer-agnostic:

import { bind } from 'typed-html-templates';

const h = (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => ({
  type,
  props,
  children
});

const html = bind(h);

Runtime behavior is delegated directly to htm.bind(h).

Type-safety model: interpolation holes

The type system parses the template string around each ${...} and infers what that hole means from context.

Hole kinds

  • Tag hole: <${X} ...>
  • Attribute value hole: <div id=${X} />
  • Spread hole: <div ...${X} />
  • Child hole: <div>${X}</div>

Each hole gets a different expected type.

1) Tag holes

In <${X} />, X must be a valid tag target:

  • intrinsic tag name ('div', 'button', etc.)
  • component function/class
const Comp = (_props: { ok: boolean }) => null;

html`<${'div'} />`; // ok
html`<${Comp} ok=${true} />`; // ok
// @ts-expect-error invalid tag target
html`<${{ nope: true }} />`;

2) Attribute value holes

In attr=${X}, X is validated against the active tag and attribute.

  • intrinsic tags use intrinsic map props
  • dynamic component tags use inferred component props
  • unknown attrs fall back to primitive values
const Card = (_props: { title: string; featured?: boolean }) => null;

html`<input value=${'ok'} checked=${true} />`; // ok
// @ts-expect-error input.value should be string
html`<input value=${123} />`;

html`<${Card} title=${'Hello'} featured=${true} />`; // ok
// @ts-expect-error title should be string
html`<${Card} title=${123} />`;

3) Spread holes

In ...${X}, X must be object-like:

  • intrinsic tag: Partial<IntrinsicProps>
  • dynamic component tag: Partial<ComponentProps>
  • allows null | undefined
const Card = (_props: { title: string; featured?: boolean }) => null;

html`<div ...${{ id: 'a' }} />`; // ok
// @ts-expect-error wrong intrinsic prop type
html`<div ...${{ id: 1 }} />`;

html`<${Card} ...${{ title: 'ok' }} />`; // ok
// @ts-expect-error wrong component prop type
html`<${Card} ...${{ title: 123 }} />`;

4) Child holes

In child position, values are constrained to ChildValue:

  • primitives (string | number | boolean | null | undefined)
  • nested child arrays
  • object-like values
html`<div>${'text'}</div>`;
html`<div>${['a', 1, { nested: true }]}</div>`;
// @ts-expect-error symbols are not valid children
html`<div>${Symbol('x')}</div>`;

Built-in DOM intrinsic typing

By default, bind(h) uses DOMIntrinsicElements derived from HTMLElementTagNameMap.

This gives out-of-the-box typing for common DOM properties, including case-insensitive attr lookup for matching (tabindex resolves against tabIndex).

import { bind } from 'typed-html-templates';

const h = (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => ({
  type,
  props,
  children
});

const html = bind(h);

html`<input value=${'x'} checked=${true} />`;
html`<div tabindex=${1} />`;
// @ts-expect-error tabindex should be number
html`<div tabindex=${'1'} />`;

Custom intrinsic map

You can replace DOM defaults with your own intrinsic map:

import { bind } from 'typed-html-templates';

type Intrinsics = {
  div: {
    id?: string;
    draggable?: boolean;
  };
  button: {
    disabled?: boolean;
    title?: string;
  };
};

const h = (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => ({
  type,
  props,
  children
});

const html = bind<typeof h, Intrinsics>(h);

html`<div id=${'ok'} draggable=${true} />`;
// @ts-expect-error id should be string
html`<div id=${123} />`;

HTM compatibility

  • Runtime rendering/parsing is from htm
  • Multiple roots, component tags, spread props, boolean attrs, etc. follow HTM runtime behavior
  • This package focuses on static typing of interpolation holes

API

  • default -> html bound to a default object-returning h
  • strictHtml -> strict-mode default export (unknown attrs rejected in known contexts)
  • bind(h) -> typed HTM-compatible tag function
  • bindStrict(h) -> typed HTM-compatible tag function that rejects unknown attrs in known contexts
  • bindSingle(h) -> typed HTM-compatible tag that enforces exactly one root at runtime
  • bindSingleStrict(h) -> strict + single-root runtime enforcement
  • single(result) -> unwraps a result | result[], throws if root count is not exactly one
  • asConst(value) -> const-generic identity helper for literal-preserving inference
  • template(parts, ...values) -> helper that preserves literal template parts and value tuple inference
  • template.from/dom/one -> aliases of template(...) for intent/readability
  • DOMIntrinsicElements -> built-in intrinsic map from HTMLElementTagNameMap
  • InferComponentProps<T> -> extract component props
  • ComponentType<Props, Result> -> component function/class shape