{"id":15608638,"url":"https://github.com/hypercubed/dynamo","last_synced_at":"2025-04-28T11:47:33.910Z","repository":{"id":31212254,"uuid":"127235001","full_name":"Hypercubed/dynamo","owner":"Hypercubed","description":"Fast dynamic method dispatch (multimethods) in TypeScript.","archived":false,"fork":false,"pushed_at":"2023-05-28T10:20:54.000Z","size":628,"stargazers_count":13,"open_issues_count":17,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-30T09:22:01.438Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Hypercubed.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-03-29T04:03:26.000Z","updated_at":"2022-08-10T15:52:24.000Z","dependencies_parsed_at":"2023-02-18T02:31:09.276Z","dependency_job_id":null,"html_url":"https://github.com/Hypercubed/dynamo","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hypercubed%2Fdynamo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hypercubed%2Fdynamo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hypercubed%2Fdynamo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hypercubed%2Fdynamo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Hypercubed","download_url":"https://codeload.github.com/Hypercubed/dynamo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251309833,"owners_count":21568913,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-03T05:21:43.776Z","updated_at":"2025-04-28T11:47:33.892Z","avatar_url":"https://github.com/Hypercubed.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dynamo\n\nFast dynamic method dispatch in TypeScript.  Easy to read and understand decorators-based function definitions are converted to runtime multimethods.  Avoids nasty runtime type checking and produces correctly typed methods.\n\n## Introduction\n\n* Compose multiple method signatures into a correctly typed dynamic dispatch function (multimethods).\n* Runtime type-checking of function arguments based on TypeScript type annotations (when possible).\n* Custom defined types coercions.\n* Easily supports union types, `any` type, and variable arguments.\n* Excellent mechanism for type constraints.\n* Extensively benchmarked and micro-optimized.\n\n\u003e Requires `experimentalDecorators` and `emitDecoratorMetadata` be enabled in your `tsconfig.json`.\n\n## TLDR Usage\n\n```ts\nimport { Dynamo, guard, conversion, signature } from '@hypercubed/dynamo';\n\nconst dynamo = new Dynamo();\n\nclass Complex {\n  @guard()\n  static isComplex(a: any): a is Complex {\n    return a instanceof Complex;\n  }\n\n  @conversion()\n  static fromNumber(a: number): Complex {\n    return new Complex(a, 0);\n  }\n\n  constructor(public re: number, public im: number) {}\n\n  add(b: Complex): Complex {\n    const re = this.re + b.re;\n    const im = this.im + b.im;\n    return new Complex(re, im);\n  }\n}\n\ndynamo.add(Complex);\n\nclass Add {\n  name = 'add';\n\n  @signature()\n  number(a: number, b: number): number {\n    return a + b;\n  }\n\n  complex(a: number | Complex, b: number | Complex): Complex;\n\n  @signature()\n  complex(a: Complex, b: Complex): Complex {\n    return a.add(b);\n  }\n}\n\n// typed as `((number, number) =\u003e number) \u0026 ((number | Complex, number | Complex) =\u003e Complex)`\nconst add = dynamo.function(Add);\n\nadd(3, 6);                                  // 9\nadd(new Complex(3, 0), new Complex(0, 6));  // Complex(3, 6)\nadd(3, new Complex(0, 6));                // Complex(3, 6)\n\n// @ts-ignore\nadd(3, '6');  // TypeError\n```\n\n## Usage Explanation\n\n### Dynamo instance\n\nStart by creating a `Dynamo` environment.  Types and conversions are local to this instance.\n\n```ts\nimport { Dynamo, guard, conversion, signature, Any } from '@hypercubed/dynamo';\n\nconst dynamo = new Dynamo();\n```\n\nThe `Dynamo` constructor also accepts an options object with the following options:\n\n- `types` - Instead of adding default types, uses this object or array of objects.  Passing `false` allows you to have no default types.\n- `autoadd` - If `autoadd` is true, when unknown types are encountered (either as a conversion or in a function signature) Dynamo will add them automatically.  If the type does not have a `@guard` defined an `instanceof X` guard will be used.\n\n### Signatures\n\nDynamic methods are defined using a class with one or more `@signature` decorators and the `dynamo.function` method.  The first method matching a argument signature is evaluated.\n\n```ts\nclass Add {\n  @signature()\n  strings(a: string, b: string): string {\n    return a + ' ' + b;\n  }\n\n  @signature()\n  numbers(a: number, a: number): number {\n    return a + b;\n  }\n}\n\n// correctly typed as `((a: string, a: string) =\u003e string \u0026 (a: number, a: number) =\u003e number)`\nconst add = dynamo.function(Add);\n\nadd(20, 22);             // 42\nadd('Hello', 'World');   // \"Hello World\"\n\n// @ts-ignore\nadd('Hello', 42);  // TypeError\n```\n\nThis library uses metadata reflections to infer types from the TypeScript type annotations.  Since TypeScript only supports [basic type serialization](http://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-4#3-basic-type-serialization_1) only basic types can be inferred.  Basic types defined by default are the primitives `number`, `string`, `boolean` and the constructors `Array`, `Function`, `Date`, and `RegExp`.  Types that are class constructors are are also supported but must be defined per `Dynamo` instance (see types below).\n\nTypeScript serializes both `undefined` and `null` as `void 0`, so these types should be explicitly listed in the signature.  Use the predefined class `Any` for `unknown` or `any`.\n\n```ts\nclass Inspect {\n  @signature(undefined)\n  inspectString(a: undefined): string {\n    return 'a is undefined';\n  }\n\n  @signature(null)\n  inspectNull(a: null): string {\n    return 'a is null';\n  }\n\n  @signature(Any)\n  inspectAny(a: unknown): string {\n    return 'a is something';\n  }\n}\n\n// correctly typed as `((a: undefined) =\u003e string \u0026 (a: null) =\u003e string \u0026 (a: unknown) =\u003e string)`\nconst inspect = dynamo.function(Inspect);\n\ninspect(undefined); // 'a is undefined'\ninspect(null);      // 'a is null'\ninspect('string');  // 'a is something'\n```\n\nOther types (including `any`, `unknown`, union types, and interfaces) are treated as `Object` by TypeScript type serialization.  To support more complex types the input parameter signatures must be supplied to the `signature` decorator.  For type unions use an array.  When listing explicit signatures for primitives used the built-in constructors.\n\n```ts\nclass Add {\n  @signature()\n  addNumbers(a: number, b: number): string {\n    return a + b;\n  }\n\n  @signature(String, [Number, String])\n  addStrings(a: string, b: number | string): string {\n    return '' + a + ' ' + b;\n  }\n}\n\n// correctly typed as `((a: string, b: number | string) =\u003e string) \u0026 (a: number, a: number) =\u003e number)`\nconst add = dynamo.function(Add);\n\nadd(20, 22);            // 42\nadd('Hello', 'World');  // 'Hello World'\nadd('Hello', 42);       // 'Hello 42'\n\n// @ts-ignore\nadd(20, 'World');       // TypeError\n```\n\nSignatures are inherited:\n\n```ts\nclass AddNumber {\n  @signature()\n  addNumbers(a: number, a: number): number {\n    return a + b;\n  }\n}\n\nclass AddStrings extends AddNumber {\n  @signature(String, [Number, String])\n  addStrings(a: string, b: number | string): string {\n    return '' + a + ' ' + b;\n  }\n}\n\n// has the type of `((a: number, a: number) =\u003e number) \u0026 ((a: string, b:  number | string) =\u003e string)`\nconst add = dynamo.function(Print);  \n\nadd(20, 22);            // 42\nadd('Hello', 'World');  // 'Hello World'\nadd('Hello', 42);       // 'Hello 42'\n\n// @ts-ignore\nadd(20, 'World');       // TypeError\n```\n\nNote that the type of the resulting function is determined by the TypeScript type annotations for each method, regardless if the `@signature` decorator was applied to it.  However, the runtime function only includes the methods to which `@signature` was applied.\n\n### Types\n\nRuntime types are defined using the `@guard` decorator and added to a dynamo instance using `dynamo.add`.  Guards are defined using static methods on a class and should be pure functions returning a boolean.  Types (guards) must be explictly assoaciated with a `dynamo` instance (unless using `autoadd`) and must be added to each `dynamo` instance it will be used.\n\n```ts\nclass Complex {\n  @guard()\n  static isComplex(x: unknown): x is Complex {\n    return x instanceof Complex;\n  }\n}\n\ndynamo.add(Complex);\n```\n\n`dynamo.add` also works as a decorator:\n\n```ts\n@dynamo.add\nclass Complex {\n  @guard()\n  static isComplex(x: unknown): x is Complex {\n    return x instanceof Complex;\n  }\n}\n```\n\n#### Type Constraints\n\nYou can add runtime constraints to primitives by extending the primitive constructor.\n\n```ts\nclass Integer extends Number {\n  @guard()\n  static isInteger(x: unknown): x is Integer {\n    return typeof x === 'number' \u0026\u0026 Number.isInteger(x);\n  }\n}\n\ndynamo.add(Integer);\n```\n\nGuards defined on classes are inherited.\n\n```ts\nclass Integer extends Number {\n  @guard()\n  static isInteger(x: unknown): x is Integer {\n    return typeof x === 'number' \u0026\u0026 Number.isInteger(x);\n  }\n}\n\nclass Even extends Integer {\n  @guard()\n  static isEven(x: number): x is Even {\n    // isInteger guard on `Integer` is invoked before isEven\n    return x % 2 === 0;\n  }\n}\n\ndynamo.add(Even);\n```\n\nIn the examples above the runtime type guards exists on the class itself, this is the default when no argumenst are bassed to the `guard` decorator.  Guards can be added for other classes by passing the class to the `guard` decorator.\n\n```ts\nimport Decimal from 'decimal.js';\n\nclass Numbers {\n  @guard(Decimal)\n  static isDecimal(x: unknown): x is Decimal {\n    return x instanceof Decimal;\n  }\n\n  @guard(BigInt)\n  static isBigInt(x: unknown): x is BigInt {\n    return typeof x === 'bigint';\n  }\n}\n\ndynamo.add(Numbers);\n```\n\nIn these cases the definitions are not attached to the type class.\n\n#### Complex Types and Interfaces\n\nAs mentioned above, TypeScript does not serialize complex types, for example this will not work as expected since TypeScript will output the type metadata for the parameter `a` as `Object`.\n\n```ts\nclass Fn {\n  @signature()\n  nope(a: string | string[]): string {\n    return 'Nope';\n  }\n}\n```\n\nA solution for this is to define a class that can act as the type definition for `string | string[]` similar to adding constraints as discussed above.\n\n```ts\nclass StringOrStringArray {\n  @guard()\n  static isStringArray(a: unknown): boolean {\n    return Array.isArray(a) ? x.every(x =\u003e typeof x === 'string') : typeof x === 'string';\n  }\n}\n\ndynamo.add(StringOrStringArray);\n\nclass Fn {\n  @signature(StringOrStringArrayGuard)\n  ok(a: string | string[]): string {\n    return 'ok';\n  }\n}\n```\n\nUsing the following trick we can define a type that will serialize correctly by TypeScript and minimize redundancy.\n\n```ts\nclass StringOrStringArrayGuard {\n  @guard()\n  static isStringArray(a: unknown): a is (string | string[]) {\n    return Array.isArray(a) ? a.every(x =\u003e typeof x === 'string') : typeof a === 'string';\n  }\n}\n\n// tslint:disable-next-line:variable-name\nconst StringOrStringArray =  StringOrStringArrayGuard;\ntype StringOrStringArray = string | string[];\n\ndynamo.add(StringOrStringArray);\n\nclass Fn {\n  @signature()\n  ok(a: StringOrStringArray): string {\n    return 'ok';\n  }\n}\n```\n\nThis will work for interfaces as well.\n\n```ts\ninterface IPerson {\n  name: Name;\n  age: Age;\n}\n\nclass PersonGuard {\n  @guard()\n  static isPerson(x: unknown): x is IPerson {\n    return typeof x === 'object' \u0026\u0026 'name' in x \u0026\u0026 'age' in x;\n  }\n}\n\n// tslint:disable-next-line:variable-name\nconst Person = PersonGuard;\ntype Person = IPerson;\n\ndynamo.add(Person);\n\nclass GetName {\n  @signature()\n  getName(person: Person): Name {\n    return person.name;\n  }\n}\n```\n\n### Conversions\n\nRuntime conversions (coursions) are added using the `@conversion` decorator and the `dynamo.add` method (or as a decorator).\n\n```ts\nclass Complex {\n  @guard()\n  static isComplex(a: any): a is Complex {\n    return a instanceof Complex;\n  }\n\n  @conversion()\n  static fromNumber(a: number): Complex {\n    return new Complex(a, 0);\n  }\n\n  constructor(public re: number, public im: number) {}\n}\n\ndynamo.add(Complex);\n```\n\nWhen defining the function, add an override to the type to get the correct TypeScript definition, Dynamo will handle the conversion.\n\n```ts\nclass add {\n  name = 'add';\n\n  add(a: number | Complex, b: number | Complex);\n\n  @signature()\n  add(a: Complex, b: Complex): Complex {\n    return a.abb(b);\n  }\n}\n\n// typed as (a: number | Complex, b: number | Complex) =\u003e Complex\nconst add = dynamo.function(Add);\n```\n\nAs mentioned above, methods are invoked with priority from top to bottom.  Note in this case the `number` method is evoked if both arguments are numbers, the complex method is invoked when one or both are are `Complex` instances.\n\n```ts\nclass add {\n  name = 'add';\n\n  @signature()\n  number(a: number, b: number): number {\n    return a + b;\n  }\n\n  complex(a: number | Complex, b: number | Complex);\n\n  @signature()\n  complex(a: Complex, b: Complex): Complex {\n    return a.abb(b);\n  }\n}\n\n// typed as `((a: number, b: number) =\u003e number \u0026 (a: number | Complex, b: number | Complex) =\u003e Complex)`\nconst add = dynamo.function(Add);\n\nadd(20, 22);                                  // 42\nadd(new Complex(20, 0), new Complex(0, 22));  // Complex(20, 22)\ntimes(20, new Complex(0, 22));                // Complex(20, 22)\n\n// @ts-ignore\ntimes(20, '22');  // TypeError\n```\n\n# License\n\nThis project is licensed under the MIT License - see the LICENSE file for details\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhypercubed%2Fdynamo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhypercubed%2Fdynamo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhypercubed%2Fdynamo/lists"}