Pretext is written in TypeScript and ships its own type definitions. This page is the complete TypeScript reference for the library: the actual type signatures of every public API, the generics and type narrowing patterns that hold up in practice, and the strict-mode considerations to be aware of in production codebases.
If you searched "pretext typescript" or "pretext ts", you're probably evaluating whether the library will play nicely with your strict TypeScript setup. The short answer: yes, it does. The longer answer fills the rest of this page.
The eight functions that make up Pretext's surface area, with their TypeScript signatures:
// Core
function prepare(text: string, font: string, options?: PrepareOptions): Prepared;
function layout(prepared: Prepared, maxWidth: number, lineHeight?: number): LayoutResult;
function layoutWithLines(prepared: Prepared, maxWidth: number, lineHeight?: number): LayoutWithLinesResult;
// Advanced
function prepareWithSegments(text: string, font: string, options?: PrepareOptions): PreparedWithSegments;
function walkLineRanges(prepared: Prepared, maxWidth: number, callback: LineCallback): void;
function layoutNextLine(prepared: Prepared, start: number, maxWidth: number): NextLineResult;
// Utility
function clearCache(): void;
function setLocale(locale?: string): void;
The supporting types:
interface PrepareOptions {
whiteSpace?: 'normal' | 'pre-wrap';
wordBreak?: 'normal' | 'break-all';
// forward-compatible: additional options may be added
}
interface LayoutResult {
height: number; // in px
lineCount: number;
}
interface LineInfo {
text: string;
width: number; // in px
start: number; // character index inclusive
end: number; // character index exclusive
}
interface LayoutWithLinesResult extends LayoutResult {
lines: LineInfo[];
}
interface NextLineResult {
end: number; // character index where the line ended (exclusive)
width: number; // in px
}
type LineCallback = (start: number, end: number, lineIndex: number) => void;
// Opaque types — don't depend on internal shape
type Prepared = { readonly __brand: 'Prepared' };
type PreparedWithSegments = Prepared & { readonly segments: readonly Segment[] };
interface Segment {
start: number;
end: number;
width: number;
kind: 'word' | 'whitespace' | 'break-opportunity' | 'cjk' | 'emoji';
}
The Prepared type is intentionally opaque — branded so TypeScript won't let you confuse it with a plain object, but with no public shape you can introspect. This is deliberate. The internal representation may change between versions, and depending on its shape would lock you to a specific Pretext version.
If you're consistently measuring text from a typed domain object (chat messages, blog posts, log entries), wrap Pretext in a generic helper to keep the call sites clean:
import { prepare, layout, type Prepared, type LayoutResult } from '@chenglou/pretext';
interface MeasurableLike {
text: string;
font: string;
}
function prepareItem<T extends MeasurableLike>(item: T): T & { prepared: Prepared } {
return { ...item, prepared: prepare(item.text, item.font) };
}
function measure<T extends { prepared: Prepared }>(
item: T,
width: number,
lineHeight: number = 1.4
): T & LayoutResult {
const dims = layout(item.prepared, width, lineHeight);
return { ...item, ...dims };
}
Now your call sites are type-safe and self-documenting:
const message: ChatMessage = { id: '1', text: 'Hi', font: '14px Inter', sender: 'alice' };
const prepared = prepareItem(message); // type: ChatMessage & { prepared: Prepared }
const measured = measure(prepared, 320); // type: ChatMessage & { prepared: Prepared } & LayoutResult
Pretext plays well with strict: true in tsconfig.json. The handful of considerations:
strictNullChecks: nothing in Pretext returns null or undefined under normal usage. The exception is the optional lineHeight parameter, which defaults to a sensible value. Always pass it explicitly if you have a project-wide line-height constant — being explicit is cheaper than chasing a "where did this default come from" question later.
noUncheckedIndexedAccess: when iterating over lines: LineInfo[], TypeScript with this flag will narrow lines[i] to LineInfo | undefined. This is correct — you should bounds-check or use array methods (map, forEach) that don't expose indexed access.
exactOptionalPropertyTypes: PrepareOptions is fine under this flag. Don't pass undefined for whiteSpace or wordBreak; just omit them.
useUnknownInCatchVariables: irrelevant — Pretext doesn't throw under normal usage. The one path that can throw is prepare() with an invalid font string, which produces a thrown Error with a descriptive message.
For applications that prepare a lot of text (chat clients, virtual lists), you'll want to cache prepare() results yourself instead of relying on the library's internal cache. Here's a typed wrapper:
import { prepare, type Prepared } from '@chenglou/pretext';
class PreparedCache {
private cache = new Map<string, Prepared>();
get(text: string, font: string): Prepared {
const key = `${font}\u0000${text}`;
let prepared = this.cache.get(key);
if (!prepared) {
prepared = prepare(text, font);
this.cache.set(key, prepared);
}
return prepared;
}
clear(): void {
this.cache.clear();
}
delete(text: string, font: string): void {
const key = `${font}\u0000${text}`;
this.cache.delete(key);
}
}
export const preparedCache = new PreparedCache();
Use \u0000 as the separator because it can't appear in a font string and is unlikely to appear in user text — it's safer than | or : which can both appear in real strings.
Pretext returns number for widths and heights. If your codebase treats CSS pixels and device pixels distinctly (it should), brand them:
type CssPx = number & { readonly __brand: 'CssPx' };
type DevicePx = number & { readonly __brand: 'DevicePx' };
function asCss(n: number): CssPx { return n as CssPx; }
function asDevice(n: number): DevicePx { return n as DevicePx; }
function toDevicePx(css: CssPx, dpr: number = devicePixelRatio): DevicePx {
return asDevice(css * dpr);
}
// Usage
import { layout } from '@chenglou/pretext';
function measureCss(prepared: Prepared, width: CssPx): { height: CssPx; lineCount: number } {
const r = layout(prepared, width as number);
return { height: asCss(r.height), lineCount: r.lineCount };
}
Pretext returns CSS pixels. If your renderer needs device pixels (canvas with manual DPR scaling, for example), convert at the boundary, not throughout.
prepareWithSegmentsprepareWithSegments returns a PreparedWithSegments, which is a Prepared with a segments array. Because PreparedWithSegments extends Prepared, you can pass it to layout() and layoutWithLines() without conversion — TypeScript will let you. But the segments are only accessible if you've used the right prepare function, so the type system will warn you if you try to read .segments off the basic Prepared type.
import { prepareWithSegments, layout, type PreparedWithSegments } from '@chenglou/pretext';
const p: PreparedWithSegments = prepareWithSegments("Hello world", "16px Inter");
const dims = layout(p, 200); // OK — PreparedWithSegments extends Prepared
console.log(p.segments[0]); // OK — segments only on PreparedWithSegments
A short triage list:
"Cannot find module '@chenglou/pretext'": npm install @chenglou/pretext. If it's installed but TS still complains, restart the TS server (the package is ESM-first; some IDEs need a kick).
"Argument of type 'string | undefined' is not assignable to parameter of type 'string'": usually text from a discriminated union. Narrow the union before calling prepare().
"Property 'segments' does not exist on type 'Prepared'": you used prepare() instead of prepareWithSegments().
"Type 'number' is not assignable to type 'CssPx'": branded type collision. Convert at the boundary with the asCss helper above.
Pretext is ESM-first with a CJS fallback for Node-side usage. In tsconfig.json, the relevant settings are:
{
"compilerOptions": {
"moduleResolution": "bundler", // or "node16" / "nodenext"
"module": "esnext",
"target": "es2020"
}
}
If you're on moduleResolution: "node" (the legacy setting), the package will still resolve via the main field, but you'll be using the CJS build, which may interact poorly with tree-shaking. Upgrade to bundler or node16 for the smallest bundle.
A small ESLint config that catches the common Pretext misuse:
{
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.name='prepare'] > Identifier",
"message": "Memoize prepare() — calling it inside a render path defeats the cache."
}
]
}
}
This is a heuristic — adjust the selector to your conventions — but the principle holds: prepare() should not be called in a tight loop or inside a render function without memoization.
If you want a React-specific TypeScript integration, the Pretext + React guide has hooks with full types. If you want the library overview without the type focus, see the Pretext library page. The repository is at github.com/chenglou/pretext and the npm package is at @chenglou/pretext.
pretext.cool is a community-maintained showcase, not affiliated with Cheng Lou or the official Pretext project.