/* A Dispatcher is a collection of operations that do run-time * dispatch on the types of their arguments. Thus, every individual * method is like a typed-function (from the library by that name), * but they can depend on one another and on ona another's implementations * for specific types (including their own). */ // First helper types and functions for the Dispatcher type TypeName = string type Parameter = TypeName type InputSignature = Parameter[] type DependenciesType = Record export type typeOfDependency = {typeOf: (x: unknown) => TypeName} // All of the implementations must publish descriptions of their // types into the following interface, using the format // described just below: export interface ImpTypes {} /***** To describe one implementation for a hypothetical operation `foo`, there should be a property of the interface whose name starts with `foo` and whose next character, if any, is an underscore. The type of this property must be the type of that implementation when T matches the first parameter of the implementation. Thus to describe an implementation that takes a number and a string and returns a boolean, for example, you could write ``` declare module "Dispatcher" { interface ImpTypes { foo_example: (a: number, b: string) => boolean } } ``` If there is another, generic implementation that takes one argument of any type and returns a Vector of that type, you can say ``` ... foo_generic: (a: T) => Vector ... ``` In practice, each subdirectory corresponding to a type, like Complex, defines an interface, like `ComplexImpTypes` for the implementations in that subdirectory, which can mostly be defined without suffixes because there's typically just a single implementation within that domain. Then the module responsible for collating all of the implementations for that type inserts all of the properties of that interface into `ReturnTypes` suitably suffixed to avoid collisions. Note that if the type is not generic, it will not bother with the generic parameter in its subdirectory ImpTypes interface. And note again, that for generic types, the type parameter of ImpTypes _must_ match the type of the **first** argument of the operation. Hence, choose argument order so that you can infer all the other parameter types and the return type from the type of the first argument. *****/ // Helper for collecting return types // (Really just adds the literal string Suffix onto the keys of interface IFace) export type ForType = keyof IFace extends string ? {[K in keyof IFace as `${K}_${Suffix}`]: IFace[K]} : never //dummy implementation for now export function joinTypes(a: TypeName, b: TypeName) { if (a === b) return a return 'any' } // Used to filter keys that match a given operation name type BeginsWith = Name | `${Name}_${string}` export type RawDependency = {[K in keyof ImpTypes]: K extends BeginsWith ? ImpTypes[K] extends (...args: Params) => any ? ImpTypes[K] : never : never} // The type of an implementation (with dependencies satisfied, // based on its name and the parameters it takes export type ImpType = RawDependency[keyof ImpTypes] // The type of a dependency on an implementation based on its name // and the parameters it takes (just a simple object with one property // named the same as the operation, of value type equal to the type of // that implementation. These can be `&`ed together in case of multiple // dependencies: export type Dependency = {[N in Name]: ImpType} // Look up the return type of an implementation based on its name // and the parameters it takes export type ImpReturns = ReturnType> // Now types used in the Dispatcher class itself type TypeSpecification = { before?: TypeName[], test: ((x: unknown) => boolean) | ((d: DependenciesType) => (x: unknown) => boolean), from: Record, infer?: (d: DependenciesType) => (z: unknown) => TypeName } type SpecObject = Record type SpecificationsGroup = Record export class Dispatcher { installSpecification( name: string, signature: InputSignature, returns: TypeName, dependencies: Record, behavior: Function // possible todo: constrain this type based // on the signature, return type, and dependencies. Not sure if // that's really possible, though. ) { console.log('Pretending to install', name, signature, '=>', returns) //TODO: implement me } installType(name: TypeName, typespec: TypeSpecification) { console.log('Pretending to install type', name, typespec) //TODO: implement me } constructor(collection: SpecificationsGroup) { for (const key in collection) { console.log('Working on', key) for (const identifier in collection[key]) { console.log('Handling', key, ':', identifier) const parts = identifier.split('_') if (parts[parts.length - 1] === 'type') { parts.pop() const name = parts.join('_') this.installType( name, collection[key][identifier] as TypeSpecification) } else { const name = parts[0] this.installSpecification( name, ['dunno'], 'unsure', {}, collection[key][identifier] as Function) } } } } }