Last active
April 7, 2023 07:44
-
-
Save prescience-data/1a7fe6a6c43766808e51707559e3617e to your computer and use it in GitHub Desktop.
Typesafe composition
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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") | |
}) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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