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

@sdxc/types

v2026.9.11

Published

Shared TypeScript types

Readme

@sdxc/types

TypeScript utility types for async return values, JSON boundaries, and type-level checks.

Installation

npm add @sdxc/types

Types only: the package ships no runtime code, so import everything with import type.

Usage

Extract the resolved type of an async function

import type { ResolvedType } from "@sdxc/types";

async function fetchUser(id: string): Promise<{ name: string; email: string }> {
	// ...
}

type User = ResolvedType<typeof fetchUser>; // { name: string; email: string }

Type component props from a data function

import type { ResolvedType } from "@sdxc/types";

import type { listPosts } from "./posts";

// Props stay in sync with whatever listPosts returns
interface Props {
	posts: ResolvedType<typeof listPosts>;
}

Combine it with indexed access to reach nested types, such as ResolvedType<typeof listPosts>["posts"][number] for a single item.

Constrain an argument to valid JSON

Take JSONValue as a bound rather than as the parameter type. The caller still gets their own shape back, and anything JSON cannot carry is rejected where it is passed.

import type { JSONValue } from "@sdxc/types";

function enqueue<T extends JSONValue>(payload: T): T {
	return payload;
}

let job = enqueue({ id: 1, tags: ["news"], draft: false });
job.tags; // string[] — the literal shape survives

enqueue({ when: new Date() }); // Error: Date is not a JSONValue
enqueue({ run: () => 1 }); // Error: functions are not a JSONValue
enqueue({ missing: undefined }); // Error: undefined is not a JSONValue

Constrain an argument to what JSON can write

JSONSerializable is the same union plus objects carrying a toJSON, so a value that substitutes itself on the way out is accepted where JSONValue rejects it.

import type { JSONSerializable } from "@sdxc/types";

function write<T extends JSONSerializable>(payload: T): string {
	return JSON.stringify(payload);
}

write({ publishedAt: new Date() }); // "{"publishedAt":"2026-09-04T00:00:00.000Z"}"
write({ href: new URL("https://example.com") });
write({ run: () => 1 }); // Error: functions are not a JSONSerializable

API

ResolvedType<T>

Unwraps the value an async function resolves to, where T is a function type (...args: any) => Promise<any>.

type User = ResolvedType<typeof fetchUser>;
// same as
type User = Awaited<ReturnType<typeof fetchUser>>;

JSONValue

Any JSON-serializable value. The union recurses into itself, so it is the one type here with no shorthand to expand — writing it inline means writing it out in full:

type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };

Reach for it as a generic bound. As a parameter type it widens the argument to the whole union and the caller loses their shape; as a constraint it only rules values out:

function enqueue<T extends JSONValue>(payload: T): T;
// payload stays { id: number; tags: string[] }

function enqueue(payload: JSONValue): JSONValue;
// payload is now the union, and `payload.id` no longer exists

JSONSerializable

Any value JSON.stringify accepts. Adds one branch to JSONValue — an object that returns its own replacement from toJSON — so it covers Date, URL and every class that serializes itself:

type JSONSerializable =
	| string
	| number
	| boolean
	| null
	| JSONSerializable[]
	| { [key: string]: JSONSerializable }
	| { toJSON(): JSONSerializable };

The two types name the two directions of one boundary, and the direction decides which one applies. Take JSONSerializable where a value is written, and JSONValue where one is read back, because the replacement is what a reader receives:

JSON.parse(JSON.stringify({ at: new Date() })).at; // a string, not a Date

JSONSerialized<T>

What a JSONSerializable type becomes after the round trip. Use it to type the read side of a boundary in terms of what was written, instead of widening to JSONValue and casting back:

type Stored = JSONSerialized<{ id: number; publishedAt: Date }>;
//   { id: number; publishedAt: string }

It applies toJSON, drops a property JSON cannot write, and writes an unwritable array element as null, since dropping a slot would change the length that is read back:

JSONSerialized<Date>; // string
JSONSerialized<{ id: number; edit: () => void }>; // { id: number }
JSONSerialized<undefined[]>; // null[]
JSONSerialized<[string, Date]>; // [string, string]

A type that already survives the round trip is returned unchanged, so JSONSerialized<T> is T for every JSONValue.

It describes the shape rather than the value. A cycle throws, NaN and Infinity read back as null, and a property that is inherited rather than owned is kept by the type and left out by JSON.stringify:

class Money {
	constructor(private cents: number) {}
	get dollars() {
		return this.cents / 100;
	}
}

JSONSerialized<Money>; // { readonly dollars: number }
JSON.stringify(new Money(500)); // {"cents":500}

It tracks nine levels of nesting and widens to JSONValue below that, which is what lets a generic constrained to JSONSerializable be passed through it — both types are recursive, and unbounded the pair exhausts the compiler rather than any real value.

IsAny<T>

Resolves to true when T is any, and false for every other type. Use it to branch on values that type as any, such as the result of JSON.parse.

type Parsed<T> = IsAny<T> extends true ? unknown : T;
// same as
type Parsed<T> = (0 extends 1 & T ? true : false) extends true ? unknown : T;

0 extends 1 & T holds only for any, because intersecting with any collapses 1 & T back to any, which 0 does extend. Every other type leaves 1 & T incompatible with 0.

type A = IsAny<any>; // true
type B = IsAny<unknown>; // false
type C = IsAny<string>; // false

Pattern: Typing deferred data

A data function can hand back a promise instead of awaiting it, so the caller decides when to resolve. ResolvedType names the value on the far side of that promise, letting the consumer type itself without restating the shape.

import type { ResolvedType } from "@sdxc/types";

import { listPosts } from "./posts";

function load() {
	return { posts: listPosts() }; // a promise, not awaited
}

interface PostListProps {
	posts: ResolvedType<typeof listPosts>["posts"];
}

function PostList(props: PostListProps) {
	// props.posts is fully typed
}

let { posts } = load();
posts.then((data) => PostList({ posts: data.posts }));

Pattern: A JSON-safe boundary

Anything crossing a serialization boundary — a queue message, a cache entry, a stored column — has to survive JSON.stringify and come back intact. Constraining the write side to JSONValue moves that from a runtime surprise to a compile error, while the read side still knows the shape it wrote.

import type { JSONValue } from "@sdxc/types";

class Queue {
	async push<T extends JSONValue>(topic: string, message: T): Promise<void> {
		await this.transport.send(topic, JSON.stringify(message));
	}
}

let queue = new Queue();

await queue.push("posts", { id: 1, publishedAt: "2026-09-04" });
await queue.push("posts", { id: 1, publishedAt: new Date() }); // Error, caught here

The second call fails at the call site rather than surviving as "2026-09-04T00:00:00.000Z" and coming back a string the consumer expected to be a Date.

A queue that formats its own dates wants the other type on the way in, and still hands JSONValue to whoever reads the message:

import type { JSONSerializable, JSONValue } from "@sdxc/types";

class Queue {
	async push<T extends JSONSerializable>(topic: string, message: T): Promise<void> {
		await this.transport.send(topic, JSON.stringify(message));
	}

	async pull(topic: string): Promise<JSONValue> {
		return JSON.parse(await this.transport.receive(topic));
	}
}

await queue.push("posts", { id: 1, publishedAt: new Date() }); // accepted

push takes the Date; pull types the message as what JSON actually carries, so the reader has to narrow the field rather than assume a Date came back.

Pattern: Typing array items from query results

Indexed access reaches into the resolved value, so a single item of a returned array gets a name of its own.

import type { ResolvedType } from "@sdxc/types";

import type { listPosts } from "./posts";

type Post = ResolvedType<typeof listPosts>["posts"][number];

interface PostRowProps {
	post: Post;
}

Versioning

Releases are dated rather than semantic. A version is the UTC date it was published, written YYYY.M.D, so 2026.9.4 is the release from 4 September 2026. At most one release goes out per day.

Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.

Depend on one exact date, and move it when you are ready to take the change:

{
	"dependencies": {
		"@sdxc/types": "2026.9.4"
	}
}

A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.

License

MIT

Author

Sergio Xalambrí