Skip to content

Instantly share code, notes, and snippets.

@prescience-data
Last active April 7, 2023 07:44
Show Gist options
  • Save prescience-data/1a7fe6a6c43766808e51707559e3617e to your computer and use it in GitHub Desktop.
Save prescience-data/1a7fe6a6c43766808e51707559e3617e to your computer and use it in GitHub Desktop.
Typesafe composition
/**
* This is a test to show how to compose objects with typescript.
*/
/**
* Creates an intersection type from a union type.
* @public
*/
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never
/**
* Creates a new type that extends the base type with the plugins.
* @public
*/
export type Composer<Base, Plugins extends Readonly<any[]>> = Base &
UnionToIntersection<Plugins[number]>
/**
* Composes an object with plugins injected type-safely.
* @param base - The base object to compose.
* @param plugins - The plugins to inject into the base object.
* @public
*/
export const compose = <Base, Plugins extends Readonly<any[]>>(
base: Base,
plugins: Plugins
): Composer<Base, Plugins> =>
plugins.reduce(
(builder, plugin) => Object.assign(builder, plugin),
base
) as Composer<Base, Plugins>
/**
* Test suite for composer types.
*/
describe("composer types", () => {
// 1. Define base object to compose into.
const base = {
zero: "zero",
crash: async () => "crash"
}
// 2/ Use `composer` function to inject plugins with types accumulation.
const composed = compose(base, [
{
nikon: "nikon",
razor: async () => "razor"
},
{
plague: "plague"
},
{
// nikon: "nikon2", <-- Attempting to redefine an existing property will cause a type error.
cereal: () => "cereal"
}
] as const) // <-- This is required to make the type inference work.
// Property tests.
it("should have all properties", () => {
expect(composed.zero).toBe("zero")
expect(composed.nikon).toBe("nikon")
expect(composed.plague).toBe("plague")
})
// Method tests.
it("should have all methods", async () => {
expect(await composed.crash()).toBe("crash")
expect(await composed.razor()).toBe("razor")
expect(composed.cereal()).toBe("cereal")
})
})
interface Task {
id: string
process: () => any
}
type TaskArray = readonly Task[]
type FulfilledDict<T extends TaskArray> = {
[K in T[number]["id"]]: {
data: Extract<T[number], { id: K }>["process"] extends () => infer R
? R
: never
}
}
class TaskRunner<T extends TaskArray> {
public readonly fulfilledDict: FulfilledDict<T>
readonly #tasks: T
public constructor(tasks: T) {
this.#tasks = tasks
}
public runAll(): void {
// Run all tasks. (This is not the focus of this test.)
}
}
export class BarcodeStatus {
public readonly id = "barcode:status"
public process() {
return "foobar" as const
}
}
export class AudioDynamicsCompressor {
public readonly id = "audio:dynamicsCompressor"
public process() {
return 123 as const
}
}
const runner = new TaskRunner([
new AudioDynamicsCompressor(),
new BarcodeStatus()
] as const)
runner.runAll()
// TypeScript will now infer the types correctly
export const audioDynamicsResult =
runner.fulfilledDict["audio:dynamicsCompressor"].data // Type: string[]
export const audioModeResult = runner.fulfilledDict["barcode:status"].data // Type: number
interface Task {
key: string
process: () => any
}
type TaskArray = readonly Task[]
type FulfilledDict<T extends TaskArray> = {
[K in T[number]["key"]]: {
data: Extract<T[number], { key: K }>["process"] extends () => infer R
? R
: never
}
}
class TaskRunner<T extends TaskArray> {
public fulfilledDict: FulfilledDict<T>
readonly #tasks: T
constructor(tasks: T) {
this.#tasks = tasks
}
public runAll(): void {
// Run all tasks. (This is not the focus of this test.)
}
}
const runner = new TaskRunner([
{
key: "audio:dynamics",
process: () => ["foo", "bar"]
},
{
key: "audio:mode",
process: () => 100
}
] as const)
runner.runAll()
// TypeScript will now infer the types correctly
export const audioDynamicsResult = runner.fulfilledDict["audio:dynamics"].data // Type: string[]
export const audioModeResult = runner.fulfilledDict["audio:mode"].data // Type: number
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment