Fast Typescript Check logo

Fast Typescript Check

OrganizationPopular
internet-development
fast-typescript-check

Keep www-sacred's TypeScript fast to type-check and fast to run. Use when touching the ASCII/canvas animation components (the only real per-frame code here), tightening type-check wall-clock, or auditing a change for runtime or compiler regressions. Scoped to this repo — a React 19 / Next.js 16 component library plus the zero-dependency Simulacrum CLI framework.

Overview

Publisherinternet-development
Repositorywww-sacred
Skill namefast-typescript-check
Stars
1.6K
Forks
82
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by internet-development on GitHub. Read the source before you install it.

Installation

Install the Fast Typescript Check AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/internet-development/www-sacred.git /tmp/www-sacred
mkdir -p .claude/skills
cp -r /tmp/www-sacred/skills/fast-typescript-check .claude/skills/fast-typescript-check
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Fast Typescript Check in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Fast Typescript Check on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Fast Typescript Check is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

fast-typescript-check

A performance discipline for www-sacred. Every rule here is justified against this codebase: either fewer CPU cycles in the components that actually run a render loop, or faster tsc --noEmit. Nothing is cosmetic, and nothing references machinery this repo does not have.

What runs hot here

This is not a game engine. There is no THREE.js, no physics, no 60fps simulation of a world. The only per-frame code is the ASCII/canvas animation family, all driven by requestAnimationFrame:

  • components/ASCIICanvas.tsx — animated ASCII art in a <pre> of per-cell <span> elements
  • components/MatrixLoader.tsx — falling-glyph matrix effect
  • components/CanvasSnake.tsx, components/DOMSnake.tsx, components/CanvasPlatformer.tsx — interactive games
  • components/examples/OneLineLoaders.tsx, components/BarLoader.tsx, components/BlockLoader.tsx, components/BarProgress.tsx — spinners

components/ASCIICanvas.tsx is the reference implementation. It already follows most of Part 1: a pre-allocated span grid, DOM diffing against previousCharsRef / previousColorsRef, refs cached into locals before the loop, an indexed for, guarded property writes, and an IntersectionObserver that stops the loop when the element scrolls off-screen. When you write or review an animation component, hold it against that file.

Everything else in the repo — the static React components, the Simulacrum CLI framework under scripts/cli/lib/*, the Python mirror — runs once per interaction, not per frame. Part 1 does not apply there; Part 2 (compiler) does.

Conventions

These match what the repo already does. Follow them; do not invent new ones.

  • Comments use //NOTE(jimmylee): (no space after //, no @) in TS/JS, # NOTE(jimmylee): in Python. Comment the why, never the what. If the code reads clearly, delete the comment. Spell names out — candidateCount, not cnt; previousColors, not pc. A clear name removes the need for a comment.
  • This repo does not use a __private prefix. Module-private state is plain const; React-internal state is refs. Don't introduce a naming scheme the rest of the codebase doesn't share.
  • The Simulacrum framework is zero-dependency TypeScript, run via tsx with no build step. Do not import that Node-only code (it uses process.stdout) from React.

Profiling before optimizing

Find the bottleneck first. For the animation components:

  1. Open DevTools → Performance, record while an ASCIICanvas / MatrixLoader is on screen for ~5s, stop.
  2. Read the flame chart — the widest bars per frame are the cost. Sort Bottom-Up by Self Time.
  3. If Scripting dominates, the animate() callback or the diff loop is the target. If Rendering/Painting dominates, the cost is DOM mutation (too many <span> writes per frame) — tighten the diff, not the math.

performance.mark / performance.measure isolate a section without DevTools overhead:

typescript
performance.mark('asciiFrameStart');
//NOTE(jimmylee): the per-cell diff loop
performance.mark('asciiFrameEnd');
performance.measure('asciiFrame', 'asciiFrameStart', 'asciiFrameEnd');

Remove the marks after profiling — they are diagnostic instrumentation (see 1.9). At 60fps a frame is 16.67ms and the browser needs ~4ms for compositing, leaving ~12ms for JavaScript. A single ASCIICanvas fills a grid of cols * rows cells every frame; if that loop owns most of the budget, the win is in the diff, not the wave math.

Part 1 — Runtime performance (animation components only)

Every rule applies to code inside a requestAnimationFrame loop. Outside the animation family, prioritize readability.

1.1 Cache refs and property chains before the loop

ASCIICanvas reads its refs once per frame into locals, then loops:

typescript
const cols = colsRef.current;
const grid = gridRef.current;
const previousChars = previousCharsRef.current;
const previousColors = previousColorsRef.current;

for (let index = 0; index < total && index < grid.length; index++) {
  // ...reads previousChars[index], writes grid[index]
}

ref.current is a property access; inside a loop over hundreds of cells it is a repeated lookup that hoists trivially. Cache anything read more than once in a loop body.

1.2 Diff before you touch the DOM

The single biggest cost in a DOM-rendered animation is writing to the DOM. ASCIICanvas only mutates a <span> when its content actually changed:

typescript
if (cell.char !== previousChars[index]) {
  span.textContent = cell.char;
  previousChars[index] = cell.char;
}
if (cell.color !== previousColors[index]) {
  span.style.color = cell.color;
  previousColors[index] = cell.color;
}

An unguarded span.textContent = cell.char every frame forces layout work even when the value is identical. The diff turns a full-grid repaint into only the cells that moved. Never write a DOM property unconditionally in a frame loop.

1.3 Indexed for-loops, no per-frame closures

Use an indexed for. Do not call map / filter / forEach / sort in a frame loop — each takes a fresh closure allocated every frame, which is pure GC pressure. for...of invokes the iterator protocol and deoptimizes in polymorphic call sites; keep it to lifecycle code (build, cleanup, one-shot queries).

typescript
//NOTE(jimmylee): allocates a comparator object every frame — avoid in rAF
cells.sort((a, b) => a.depth - b.depth);

If a sort is genuinely needed per frame, confirm it can't be hoisted behind a change check first.

1.4 Pre-allocate; never allocate inside the frame loop

ASCIICanvas builds its span grid once in buildGrid(cols) and only rebuilds when the column count changes. Each frame reuses the existing spans and the existing previousChars / previousColors arrays. Allocation inside a frame loop creates GC pressure, and a single GC pause is a visible frame skip. Allocate buffers at setup, index into them each frame.

1.5 Guard the whole loop, not each iteration

When the component has nothing to do, skip the frame entirely. ASCIICanvas returns early when off-screen and never schedules the next frame:

typescript
const loop = () => {
  if (!visibleRef.current || cancelled) return;
  // ...
  frameRef.current = requestAnimationFrame(loop);
};

Pair this with an IntersectionObserver so a loader scrolled out of view costs zero CPU. This is the highest-leverage optimization in the repo: a page with several ASCIICanvas instances would otherwise run every one of them forever.

1.6 Bitwise floor for positive grid math

Math.floor(x) is a call; x | 0 truncates in one instruction. Safe only for positive values inside 32-bit signed range — exactly the case for grid column/row indexing.

typescript
const column = index % cols;
const row = (index - column) / cols;

ASCIICanvas derives row by exact integer division (the subtraction guarantees divisibility), which avoids Math.floor entirely. Where you do need a floor on a known-positive value, | 0 is the cheaper form.

1.7 Keep numbers in one type lane

V8 represents small integers (Smis) differently from boxed doubles. A counter that starts at 0 and later receives a float forces a representation change. In the wave math (Math.sin, Math.cos), values are doubles throughout — keep them that way and keep loop indices integer throughout. Don't mix Math.random() or division results into an integer accumulator inside a hot loop.

1.8 Guard property writes that trigger work

Setting style.color or textContent schedules style/layout work even when the value is unchanged (see 1.2). The same applies to any setter with side effects. Guard the write behind a difference check.

1.9 Strip diagnostic instrumentation from the frame path

performance.now() is fine once per frame (ASCIICanvas reads it for the time base). console.log, JSON.stringify, and extra performance.now() calls for profiling are not — at 60fps they are real cost. Gate them behind a debug flag or remove them after measuring.

1.10 Prefer as const objects over enums; ES modules over namespaces

Enums compile to runtime IIFEs with reverse-mapping tables; namespaces compile to IIFEs that block tree-shaking. as const objects produce zero runtime code and inline cleanly. This repo has no enums and no namespaces today — keep it that way. const enum is doubly wrong here because isolatedModules: true (set in tsconfig.json) cannot inline it across files.

typescript
const DIRECTION = { Up: 'UP', Down: 'DOWN', Left: 'LEFT', Right: 'RIGHT' } as const;
type Direction = (typeof DIRECTION)[keyof typeof DIRECTION];

Part 2 — Compiler performance

These reduce tsc --noEmit wall-clock and editor responsiveness. They apply across the whole repo, not just the animation family.

2.1 Add explicit return types on exported functions

Inferred return types on exports can balloon into anonymous types with import("./path").Type chains that slow editor responsiveness and incremental rebuilds. A named return type is compact.

typescript
//NOTE(jimmylee): compiler infers a wide anonymous type
export function createState() {
  return { x: computeX(), y: computeY() };
}

export function createState(): EngineState {
  return { x: computeX(), y: computeY() };
}

2.2 Prefer interface extends over & intersections at boundaries

Interfaces produce a single cached flat type; intersections re-merge on every use and detect no conflicts. Use interface extends for types that cross module boundaries. Interior types used once don't matter.

2.3 Keep union types small

Union deduplication is pairwise (quadratic). Past ~12 members, refactor to a base type with a discriminant field instead of a wide A | B | C | ... union.

2.4 Limit recursive generic nesting

Deeply nested recursive generics (DeepPartial<T> style) are a common cause of slow checks — each level multiplies instantiations. Keep recursion to ≤3 levels; for known shapes, write the concrete type. This repo has no such generics today; don't add one without measuring.

2.5 import type for type-only imports

import type is erased at runtime, shrinking the module graph the bundler walks and heading off circular-import emit problems.

typescript
import type { ASCIIAnimationFn } from '@components/ascii/utilities';

Note: this repo does not set verbatimModuleSyntax, so the compiler will not force this on you — it is a discipline, not an enforced error. Apply it to type-only imports anyway.

2.6 The real tsconfig.json

These are the settings actually in tsconfig.json and why they matter. Do not document settings the file doesn't have.

jsonc
{
  "compilerOptions": {
    "paths": { "@root/*": ["./*"], "@common/*": ["./common/*"], "@components/*": ["./components/*"], "@modules/*": ["./modules/*"] },
    "target": "es2017",              // Next.js/SWC does the real downlevel; tsc target only affects lib surface
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,                 // kept for compatibility; the Simulacrum framework is now .ts
    "skipLibCheck": true,            // skips .d.ts checking — the largest single tsc speedup
    "strict": false,                 // only strictNullChecks is on (see below)
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,                  // tsc is a checker only; Next.js/SWC emits
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",   // matches the bundler's resolution — correctness, not speed
    "resolveJsonModule": true,       // lets TypeScript import colors.json as a typed module
    "isolatedModules": true,         // every file transpiles independently (Next/SWC); blocks const enum
    "jsx": "react-jsx",
    "incremental": true,             // writes tsconfig.tsbuildinfo, skips unchanged files
    "plugins": [{ "name": "next" }],
    "strictNullChecks": true
  },
  "exclude": ["node_modules", "**/*.spec.ts"],
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"]
}

Notes that matter when editing this file:

  • strict is off; only strictNullChecks is enabled. Turning on full strict would be a large, separate diff (many components are loosely typed on purpose — see components/AGENTS.md). Don't flip it as a side effect.
  • The paths map lists only the four live aliases: @root/*, @common/*, @components/*, @modules/*. Dead aliases (@system, @demos, @data, @pages) were removed.
  • exclude lists node_modules (not the **/node_modules glob). TypeScript excludes node_modules by default, and once any exclude entry exists you must keep an explicit node_modules entry or the compiler crawls every package — which this file does.
  • There are no project references and no verbatimModuleSyntax / types: [] / moduleDetection. The repo is small enough that none of these earn their complexity yet; revisit only if time npx tsc --noEmit climbs past ~30s.

Resolved: baseUrl was dropped (modern resolution maps paths from the config file's own location) and target was raised from es5 to es2017. tsc --noEmit exits 0 with no deprecation diagnostics under TypeScript 6.x.

2.7 Compiler diagnostic commands

Run before and after any change that touches types broadly.

CommandWhat it tells you
npx tsc --noEmit --extendedDiagnosticsFiles, Bind, Check, Emit times. High Check time = type complexity.
time npx tsc --noEmitWall-clock budget check. Should stay well under 30s for this repo.
npx tsc --noEmit --generateTrace ./trace then npx @typescript/analyze-trace ./traceSlowest files and most expensive type instantiations.
npx tsc --explainFiles | grep node_modulesSurfaces packages pulled in that shouldn't be.

Part 3 — Horizon: the native TypeScript compiler

Microsoft is porting tsc to Go (shipping as TypeScript 7.0), with ~10x type-check speedups reported on large codebases. It does not change language semantics — every rule above applies equally to both compilers. The native port makes checking faster; it does not make a quadratic union cheaper to evaluate. Available now as @typescript/native-preview. This repo type-checks in seconds today, so the win here is editor latency, not CI.

Sources

In order of authority; higher-ranked sources win on conflict.

  1. Microsoft TypeScript Performance Wikihttps://github.com/microsoft/TypeScript/wiki/Performance — tsconfig settings, type-system complexity, diagnostics.
  2. TypeScript Native Port announcementhttps://devblogs.microsoft.com/typescript/typescript-native-port/ — the 10x figures.
  3. Vyacheslav Egorov, "What's up with monomorphism?"https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html — V8 inline caches and hidden-class transitions.
  4. V8 Blog — TurboFan JIThttps://v8.dev/blog/turbofan-jit — the optimizing compiler.

A note on moduleResolution: "bundler": blogs sometimes list it as a speed setting. The TS wiki does not — resolution mode affects which files are found, not how fast they check. It is the correct setting here because it matches the bundler, not because it is faster.

Audit checklist

Runtime — only for code inside a requestAnimationFrame loop:

  • Refs / property chains read more than once are cached in a local before the loop
  • DOM writes (textContent, style.*) are guarded by a difference check — diff, don't repaint
  • No new allocations and no closures (map/filter/sort callbacks) inside the loop
  • Buffers (span grid, previous-value arrays) are pre-allocated and reused, rebuilt only on resize
  • The loop early-returns when off-screen / cancelled, gated by an IntersectionObserver
  • Numeric variables stay in one lane (integer indices, double math)
  • No console.log / extra performance.now() left in the frame path

Compiler — whole repo:

  • Exported functions that return complex objects have explicit return types
  • Boundary object types use interface extends, not &
  • Unions stay under ~12 members
  • Type-only imports use import type
  • No enum, no namespace, no const enum
  • tsconfig edits keep an explicit node_modules in exclude and don't silently flip strict

Naming:

  • Names are spelled out (previousColors, not pc)
  • Comments are //NOTE(jimmylee): / # NOTE(jimmylee):, explain why, and aren't restating the code
  • No __private prefix introduced — it isn't a convention in this repo

Frequently asked questions

What does the Fast Typescript Check AI skill do?

Keep www-sacred's TypeScript fast to type-check and fast to run. Use when touching the ASCII/canvas animation components (the only real per-frame code here), tightening type-check wall-clock, or auditing a change for runtime or compiler regressions. Scoped to this repo — a React 19 / Next.js 16 component library plus the zero-dependency Simulacrum CLI framework.

Why use Fast Typescript Check on TypingMind?

Because you install it once and use it with any model. Fast Typescript Check is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Fast Typescript Check in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/internet-development/www-sacred/tree/main/skills/fast-typescript-check. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Fast Typescript Check?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Fast Typescript Check?

As many as you like. As long as a model supports skills, you can use Fast Typescript Check with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Fast Typescript Check AI skill free?

Yes. It is published on GitHub by internet-development under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇