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-asl

v0.4.1

Published

Build Amazon States Language state machines in TypeScript, with compile-time proof that every task payload matches its Lambda's Zod schema and every JSONPath ref resolves.

Readme

typed-asl

docs CI coverage npm npm provenance runtime dependencies

Generates Amazon States Language JSON from TypeScript, with compile-time proof that payload mappings match the Lambdas' Zod schemas and that every JSONPath ref resolves to a real upstream output of the right type.

npm install typed-asl zod

Why

Hand-written ASL and Lambda handler schemas are disconnected: misaligned payloads, dangling JSONPath references and type mismatches surface only at runtime, mid-execution. If a machine built here typechecks, its payloads are correct.

Caught at compile time: missing or extra payload fields, refs to a nonexistent state (ctx.doesNotExist.foo) or field, ref type mismatches, cross-branch parallel access (ctx.par[1].stateFromBranch0), out-of-range parallel indices (ctx.par[2] on a two-branch parallel), choice conditions whose variable or operand type disagrees with the operator, map item selectors that are typos or not arrays, customTask refs that point where the result doesn't live, and optional output fields without an explicit resultSelector. Known ASL error names (States.Timeout, Lambda.TooManyRequestsException, …) autocomplete in retry/catch configs. Every guarantee here has a matching negative test (src/lib/type-guarantees.test.ts) that fails the build if the API stops rejecting the bad code.

import { SequenceBuilder } from 'typed-asl';

const machine = new SequenceBuilder<Input>()
  .task(
    'runMediaInfo',
    {
      inputSchema: RunMediainfoStepInput,
      outputSchema: RunMediainfoStepOutput,
      functionArn: LAMBDA_ARN,
    },
    (ctx) => ({ bucket: ctx.bucket, key: ctx.key }),
  )
  .task(
    'createVideo',
    {
      inputSchema: CreateVideoInput,
      outputSchema: CreateVideoOutput,
      functionArn: LAMBDA_ARN,
    },
    (ctx) => ({ mediaInfo: ctx.runMediaInfo.mediaInfo }),
  )
  .build();

Each method appends a state and returns the builder widened with that state's output, so ctx at every step is a Proxied<Ctx> whose property accesses record JSONPath segments ($.runMediaInfo.mediaInfo) while carrying the schema's type.

build() returns a plain ASL object. Write it to a file, feed it to Terraform or CDK, or hand it to CreateStateMachine — the library has no opinion about deployment.

Learn it

tutorial/ is the documentation: eleven numbered files that build the library's ideas from scratch, each one a runnable test. Start at 00-the-problem.test.ts and read in order. Because they are tests, they cannot drift from the implementation.

The same eleven chapters are published at solidlabs.com/docs/typed-asl if you would rather read them in a browser.

Type machinery worth knowing

Parallel branches are a tuple, not an array. A Parallel state's ASL output is positional, and a plain Array<Union> would lose that. BranchOutputTuple is a mapped tuple ({ [I in keyof Branches]: Omit<Full, keyof Base> }), which preserves per-index types, so ctx.process[0] is branch 0's own output. Two things keep it working: the [...Branches] variadic tuple parameter (without it TypeScript infers SequenceBuilder<any>[]) and each builder's declare readonly _ctx: Ctx phantom field, which is what infer extracts.

The context accumulates as a tuple, not as nested Omits. A builder carries three type parameters — SequenceBuilder<Ctx, Base, E> — but you write one. Base is where the chain started, E is a flat tuple of the [key, output] pairs each state contributed, and Ctx is always ContextOf<Base, E>: the two of them materialized into the object you actually read. Each chained call appends to E rather than wrapping the previous context, which is what keeps long chains cheap. Writing it the obvious way — Omit<Ctx, Name> & Record<Name, Out>, folded in per call — makes the context at step N a mapped type wrapping step N-1's, and resolving that stack costs 2^N: chains failed to compile at 17 states with TS2589 (#14).

Base and E are inference state, not part of a builder's identity — they appear on no property, so builders compare on their context alone. SequenceBuilder<SomeCtx> still works as an annotation, and single-type-parameter helpers (<C>(b: SequenceBuilder<C>) => …) still match a chain that has accumulated states. The one pattern that changes is matching the type directly: T extends SequenceBuilder<infer C> ? C : never now yields never — use the exported InferContext<T> instead.

The context type hovers as a plain object. ContextOf is wrapped in Simplify<T> = { [K in keyof T]: T[K] } & {}, so your editor prints { bucket: string; key: string; loadFile: … } rather than the machinery that produced it, no matter how long the chain. Assignability is unchanged; only the rendering is. One consequence: keys appear in the mapped type's iteration order, not declaration order. A state whose key collides with an earlier one — or with a field of the starting context — replaces it rather than intersecting, matching what actually lands in the state data.

choice leaves the context type unchanged — the builder can't know which branch ran. Non-terminal branches converge on the next chained state, fail states stay terminal, empty branches skip straight to convergence, and choices nest.

pipe(fn) keeps reusable task groups in a flat chain. Declare the function generic over Ctx extends { … } and the constraint becomes its documented requirement on upstream outputs:

const addCreateAtlas = <
  Ctx extends { extractFrames: { frameStorageRefs: StorageRef[] } },
>(
  b: SequenceBuilder<Ctx>
) =>
  b.task('createAtlas', createAtlasConfig, (ctx) => ({
    frameStorageRefs: ctx.extractFrames.frameStorageRefs,
    outputFilename: 'atlas.webp',
  }));

new SequenceBuilder<Input>()
  .task('extractFrames', …)
  .pipe(addCreateAtlas)
  .task('finalize', …)
  .build();

How this is verified

Each claim above is pinned by something that fails the build when it stops being true.

  • 251 tests across the library and the tutorial, run on Node 20, 22 and 24.
  • Every build() in the suite is validated against the ASL spec with asl-validator, through a setup hook rather than per-call-site opt-in — so a fixture AWS would reject cannot pass CI.
  • Negative type tests (src/lib/type-guarantees.test.ts) pin the compile-time contract as @ts-expect-error cases. tsc checks those in both directions, so they still fail when inference quietly degrades to any — which no passing runtime test would catch.
  • Three compilers: the pinned ~5.7, the newest 5.x, and typescript@latest. Type-level behavior is this library's API surface, so a compiler upgrade can be a breaking change.
  • Coverage is a ratchet — the floor sits at 95% statements / 91% branches, and CI fails if it drops. Thresholds only move up, with one documented exception: re-baselining when a coverage-tool major bump shifts attribution (vitest 3 → 4 measured about a point lower on identical code). The badge reads lower than those figures because Codecov counts a partially-covered line as a miss where vitest counts it as hit — the same report is 91% there and 96% lines here.
  • Releases are signed. Publishing runs from the tagged workflow through npm trusted publishing (OIDC, no long-lived token), so the tarball on npm carries provenance back to this repo and commit.

Scope

Supported states: Task (Lambda, plus a customTask escape hatch for any service integration ARN), Parallel, Map, Choice, Pass, Wait, Fail, Succeed. Retry and Catch work on Task, customTask, Map, and Parallel; TimeoutSeconds/HeartbeatSeconds (and their ...Path variants) on Task and customTask. Choice supports every JSONPath-mode comparison operator, *Path variants included (typed refs on both sides). Intrinsics: the full JSONPath-mode set — Format, JsonToString, StringToJson, JsonMerge, Array, ArrayLength, ArrayGetItem, ArrayContains, ArrayRange, ArrayUnique, ArrayPartition, MathAdd, MathRandom, StringSplit, Base64Encode/Decode, Hash, UUID.

Not yet supported, and worth knowing before you adopt:

  • JSONPath mode only. The newer JSONata query language and Assign/variables are not implemented; the ref machinery assumes JSONPath.
  • No Distributed Map (ItemReader/ResultWriter/ItemBatcher).
  • Zod only. Output schemas drive ResultSelector generation, so other validators aren't pluggable today.
  • Optional output fields require an explicit resultSelector. The auto-generated selector maps every output schema key from $.Payload.{key}, and ASL errors at runtime when a referenced key is absent — so a schema with .optional() fields is rejected at compile time (and at build time) unless you pass a resultSelector selecting only what the Lambda always returns.
  • build() does not validate against the ASL spec. It guarantees your mappings and refs, not that AWS will accept every machine you can express. (The library's own test fixtures are all checked against the spec with asl-validator in CI — your machines at build time are not.)

This came out of a production monorepo, where it builds real state machines — but it has been shaped by a small number of them. Expect rough edges on shapes we haven't hit. Issues and PRs welcome.

License

MIT