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

@samrith/lit

v1.1.1

Published

Type-safe tagged template literals for TypeScript.

Downloads

1,824

Readme

Lit

Type-safe tagged template literals for TypeScript. Declare slot names and types inline, get compile-time checking and a small runtime renderer — no separate schema file.

import { lit } from "@samrith/lit";

const greet = lit`Hello, ${"name"}! You are ${"age?:number=18"} years old.`;

greet({ name: "Ada" });
// => "Hello, Ada! You are 18 years old."

greet({ name: "Ada", age: 42 });
// => "Hello, Ada! You are 42 years old."

Install

bun add @samrith/lit

Or copy lit.js into your project — the library is a single file with no runtime dependencies.

Why Lit?

String templates often mix static text with dynamic values. Common approaches leave gaps:

  • Plain template literals — no named parameters, no reuse, no validation.
  • Separate schemas — types and defaults live far from the string they describe.
  • Untyped helpers — easy to pass the wrong key or type at runtime.

Lit keeps the template and its contract in one place. Slot specs like "name" or "name:string" sit in the template literal; TypeScript infers the parameter object; optional slots and defaults are first-class.

Quick start

import { lit } from "@samrith/lit";

// Required string slot (type omitted → string)
const title = lit`Issue #${"id:number"}: ${"title"}`;
title({ id: 42, title: "Fix the build" });
// => "Issue #42: Fix the build"

// Optional string slot (omitted → empty string)
const line = lit`Status: ${"status?:"}`;
line({});
// => "Status: "

// Optional slot with default
const meta = lit`Version ${"version?:=0.0.0"}`;
meta({});
// => "Version 0.0.0"

Slot syntax

Place slot descriptors in ${...} inside a lit tagged template. Each descriptor is a string with this shape:

| Form | Meaning | | ----------------- | ------------------------------------- | | name | Required string (type omitted) | | name?: | Optional string (type omitted) | | name?:=default | Optional string with default | | name:string | Required string | | name:number | Required number | | name:boolean | Required boolean | | name?:string | Optional; undefined / null"" | | name?:number=42 | Optional with default when missing |

Rules:

  • Omit the type to default to string — use name or name?: instead of name:string / name?:string.
  • When a type is given, it must be exactly string, number, or boolean.
  • Defaults require the optional marker: name?:=default or name?:type=default (not name:type=default).
  • Default literals are parsed at template definition time (true / false for booleans, numeric literals for numbers, raw text for strings).

Invalid specs throw when the template is created:

lit`${"name:object"}`; // Error: invalid type
lit`${"name:string=hi"}`; // Error: default without ?
lit`${"name?:number=hi"}`; // Error: invalid number default

API

lit(strings, ...slots)

Tagged template function. Returns a renderer function (and supports nesting — see below).

const template = lit`Hello ${"name"}!`;

// Call with parameters
template({ name: "World" }); // "Hello World!"

Parameter typing: If every slot is optional (or the template has no slots), the renderer accepts an optional argument. If any slot is required, the argument is required and TypeScript lists the exact keys.

Exported type for the return value:

import type { LitFn } from "@samrith/lit";

const fn: LitFn<{ name: string }> = lit`${"name"}`;

Extracting parameters

Extract the parameter object type from a lit template (or any LitFn):

import { lit, type ExtractParams } from "@samrith/lit";

const tmpl = lit`Hello ${"name"}! You are ${"age?:number=18"} years old.`;

type Params = ExtractParams<typeof tmpl>;
// { name: string; age?: number | null }

Useful when you want to reuse the inferred shape elsewhere — function arguments, context types, or validation schemas — without duplicating the slot definitions.

Nested templates

Another lit template can be used as a slot value. Nested templates share the parent’s parameter object.

const emphasis = lit`<strong>${"text:string"}</strong>`;
const card = lit`<div>${emphasis} — ${"label:string"}</div>`;

card({ text: "Hi", label: "Note" });
// => "<div><strong>Hi</strong> — Note</div>"

Nested renderers receive the same params object as the outer template, so keys must not collide unless you intend to reuse a value.

Behavior

Rendering

  1. Static string segments are concatenated in order.
  2. Each slot is resolved from the params object:
    • Required / present value — coerced with String(value).
    • undefined or null on optional slot — default if set, otherwise "".
  3. Nested lit functions run with the same params.

Type inference

Lit merges parameters from all slots in a template (and from nested templates) into one object type:

  • Required keys must be supplied when calling the renderer.
  • Optional keys may be omitted or set to null.
  • Invalid slot strings are rejected at compile time via ValidSlotSpec.

Errors (runtime)

Thrown when the template is defined, not when it is rendered:

| Situation | Message (summary) | | ------------------------------ | ----------------------------------------------------------------------------- | | Malformed slot string | Expected name, name?:, name:type, name?:type, or name?:type=default | | Default on required slot | Defaults require ? | | Unknown type | Expected string, number, or boolean | | Invalid boolean/number default | Invalid default for type |

Examples

Email subject

const subject = lit`[${"project"}] ${"action"}: ${"item"}`;

subject({
  project: "lit",
  action: "PR",
  item: "README",
});
// => "[lit] PR: README"

Conditional copy with optional segment

const banner = lit`Welcome${"name?:"}!`;

banner({}); // "Welcome!"
banner({ name: ", Ada" }); // "Welcome, Ada!"  (leading comma in value if desired)

Composed SQL-style fragment (illustration only — use a proper query builder for production)

const whereId = lit`id = ${"id:number"}`;
const query = lit`SELECT * FROM users WHERE ${whereId}`;

query({ id: 1 });
// => "SELECT * FROM users WHERE id = 1"

Development

# Install dev dependencies
bun install

# Type-check
bunx tsc --noEmit lit.ts

There is no build step: ship lit.ts as source or wire it into your bundler’s TypeScript pipeline.

License

MIT © Samrith Shankar