Skip to content

Instantly share code, notes, and snippets.

@melodymorgan
Last active September 2, 2025 18:05
Show Gist options
  • Save melodymorgan/3f445024728678f935e052eedd6c8253 to your computer and use it in GitHub Desktop.
Save melodymorgan/3f445024728678f935e052eedd6c8253 to your computer and use it in GitHub Desktop.
keep expansions optional in your base type but also allow a stricter generic type when you know which expansions are present (e.g., in your client SDK)
// ---------- Base Types ----------
interface Transaction {
id: string;
amount: number;
}
interface Document {
id: string;
title: string;
}
interface Participant {
id: string;
name: string;
}
interface Process {
id: string;
step: string;
}
interface Requirement {
id: string;
description: string;
}
// ---------- Expansions Map ----------
type ExpansionsMap = {
documents: Document[];
participants: Participant[];
processes: Process[];
requirements: Requirement[];
};
// ---------- Generate EXPAND_KEYS automatically ----------
export const EXPAND_KEYS = Object.freeze(
Object.keys({} as ExpansionsMap).reduce((acc, key) => {
acc[key as keyof ExpansionsMap] = key;
return acc;
}, {} as Record<keyof ExpansionsMap, keyof ExpansionsMap>)
);
export type ExpandKey = keyof typeof EXPAND_KEYS;
// Base response: transaction always present, expansions optional
type TransactionResponseBase = {
transaction: Transaction;
} & Partial<ExpansionsMap>;
// ---------- Utility: Pretty Print ----------
type Expand<T> = { [K in keyof T]: T[K] } & {};
// ---------- Domain-Specific Expand ----------
type ExpandKeys<
Base,
Keys extends keyof ExpansionsMap
> = Expand<
Base & {
[K in Keys]-?: { [P in K]: ExpansionsMap[P] }
}[Keys] extends infer O
? { [K in keyof O]: O[K] }
: never
>;
// ---------- SDK Function ----------
async function getTransaction<
T extends ExpandKey[] = []
>(
opts?: { expand?: T }
): Promise<ExpandKeys<TransactionResponseBase, T[number]>> {
// Typed mock base
const base: TransactionResponseBase = {
transaction: { id: "txn_1", amount: 100 },
documents: [{ id: "doc_1", title: "Contract" }],
participants: [{ id: "p_1", name: "Alice" }],
processes: [{ id: "proc_1", step: "review" }],
requirements: [{ id: "req_1", description: "Signed NDA" }],
};
if (!opts?.expand) {
return { transaction: base.transaction } as ExpandKeys<
TransactionResponseBase,
T[number]
>;
}
const picked: Pick<ExpansionsMap, T[number]> = {} as Pick<
ExpansionsMap,
T[number]
>;
for (const key of opts.expand) {
picked[key] = base[key]!;
}
return { transaction: base.transaction, ...picked } as ExpandKeys<
TransactionResponseBase,
T[number]
>;
}
// ---------- Example Usage ----------
async function runExamples() {
// Expand using generated constants
const r = await getTransaction({
expand: [EXPAND_KEYS.documents, EXPAND_KEYS.participants],
});
r.documents[0].title; // ✅ required
r.participants[0].name; // ✅ required
r.processes; // ❌ TS error
}
runExamples();
// refine this so EXPAND_KEYS.documents is typed literally as "documents", instead of just string
// ---------- Expansions Map ----------
type ExpansionsMap = {
documents: { id: string; title: string }[];
participants: { id: string; name: string }[];
processes: { id: string; step: string }[];
requirements: { id: string; description: string }[];
};
// ---------- Generate EXPAND_KEYS with literal types ----------
export const EXPAND_KEYS = Object.freeze(
Object.keys({} as ExpansionsMap).reduce((acc, key) => {
acc[key as keyof ExpansionsMap] = key;
return acc;
}, {} as Record<keyof ExpansionsMap, keyof ExpansionsMap>)
) as {
readonly [K in keyof ExpansionsMap]: K;
};
// Type = "documents" | "participants" | "processes" | "requirements"
export type ExpandKey = keyof typeof EXPAND_KEYS;
// ---------- Expansions Map ----------
type ExpansionsMap = {
documents: { id: string; title: string }[];
participants: { id: string; name: string }[];
processes: { id: string; step: string }[];
requirements: { id: string; description: string }[];
};
// ---------- Generate EXPAND_KEYS with literal types ----------
export const EXPAND_KEYS = {
documents: "documents",
participants: "participants",
processes: "processes",
requirements: "requirements",
} as const;
export type ExpandKey = keyof typeof EXPAND_KEYS;
// ---------- Base Response ----------
type TransactionResponseBase = {
transaction: { id: string; amount: number };
};
// ---------- Utility: attach expansions ----------
type ExpandKeys<Base, K extends keyof ExpansionsMap> =
Base & { [P in K]: ExpansionsMap[P] };
// ---------- SDK function with literal preservation ----------
async function getTransaction<const T extends readonly ExpandKey[]>(
opts?: { expand?: T }
): Promise<
T extends readonly []
? TransactionResponseBase
: ExpandKeys<TransactionResponseBase, T[number]>
> {
const base: TransactionResponseBase & ExpansionsMap = {
transaction: { id: "txn_1", amount: 100 },
documents: [{ id: "doc_1", title: "Contract" }],
participants: [{ id: "p_1", name: "Alice" }],
processes: [{ id: "proc_1", step: "review" }],
requirements: [{ id: "req_1", description: "Signed NDA" }],
};
if (!opts?.expand?.length) {
return { transaction: base.transaction } as TransactionResponseBase;
}
// Pick expansions dynamically
const picked = Object.fromEntries(
opts.expand.map(k => [k, base[k]])
) as Pick<ExpansionsMap, T[number]>;
return { transaction: base.transaction, ...picked } as ExpandKeys<
TransactionResponseBase,
T[number]
>;
}
// usage examples
// No expansions → only transaction
const r1 = await getTransaction();
// ^? { transaction: { id: string; amount: number } }
// Expand documents only → narrowed type includes only documents
const r2 = await getTransaction({ expand: [EXPAND_KEYS.documents] });
// ^? { transaction: {...}; documents: { id: string; title: string }[] }
// Expand documents + participants → narrowed type includes both
const r3 = await getTransaction({
expand: [EXPAND_KEYS.documents, EXPAND_KEYS.participants],
});
// ^? { transaction: {...}; documents: ...; participants: ... }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment