feat: Narrow tsc typing of operation dependencies/implementations

This commit is contained in:
Glen Whitney 2024-09-29 13:48:06 -07:00
parent 90b66dc863
commit f575582879
19 changed files with 912 additions and 111 deletions

View file

@ -0,0 +1,4 @@
export type Configuration = {
epsilon: number
predictable: boolean
}

201
src/core/Dispatcher.ts Normal file
View file

@ -0,0 +1,201 @@
import type {AnyFunc, CommonSignature, GenSigs} from '@/interfaces/type'
// A base type that roughly describes the dependencies of a single factory
// for implementations of one operation. It is an object whose keys are the
// identifiers are dependencies, and whose values describe that dependency.
// In the value for a given key, the 'is' property gives the name of the
// operation that dependency should be an instance of, defaulting to the key
// itself when not present, and the 'sig' property gives the desired
// signature for that operation. When the 'sig' property is not present,
// the signature will default to some ambient ensemble of signatures.
export type RawDependencies = Record<string, {is?: string, sig?: AnyFunc}>
// The following type transform fills in any unspecified signatures in RD
// with the corresponding signatures from SomeSigs:
type PatchedDepSpec<RD extends RawDependencies, SomeSigs extends GenSigs> = {
[K in keyof RD]: RD[K] extends {sig: AnyFunc}
? RD[K]
: K extends keyof SomeSigs ? (RD[K] & {sig: SomeSigs[K]}) : RD[K]
}
// A factory for building dependency specifications from the ensemble of
// common signatures for a specific type (and perhaps auxiliary type). This
// is typically used when describing implementation factories for one type
// that depend on the common signatures for a *different* type.
export function commonSpecs<
T,
Aux = T,
CommonSigs extends GenSigs = CommonSignature<T, Aux>
>() {
return <RD extends RawDependencies>(
rd: RD
): PatchedDepSpec<RD, CommonSigs> => Object.fromEntries(
Object.keys(rd).map(k => [
k, 'sig' in rd[k] ? rd[k]
: {...rd[k], sig: (() => undefined)}
])
) as PatchedDepSpec<RD, CommonSigs>
}
// Further constraint on a dependency specification that means it is ready
// to use with a given set of signatures:
type DepSpec<Signatures extends GenSigs, Needs extends string>
= {
[K in Needs]: K extends keyof Signatures
? {sig?: AnyFunc}
: {is: keyof Signatures, sig: AnyFunc}
}
// Just checks if an RawDependencies is really a DepSpec, and blanks it out if not
type DepCheck<
RD extends RawDependencies,
Signatures extends GenSigs,
Needs extends string = keyof RD & string
> = RD extends DepSpec<Signatures, Needs> ? RD
: {
[K in Needs]: K extends keyof Signatures ? {}
: {is: never, sig: (q: boolean) => void}
}
// The actual type of a dependency, given a dependency specification
type DepType<
Signatures extends GenSigs,
DS extends DepSpec<Signatures, (keyof DS & string)>
> = {[K in keyof DS]: DS[K] extends {sig: AnyFunc}
? DS[K]['sig']
: K extends keyof Signatures ? Signatures[K] : never
}
// A collection of dependency specifications for some of the operations in
// an ensemble of Signatures:
type Specifications<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>
> = {[K in NeedKeys]: DepSpec<Signatures, NeedList[K]>}
// The type of a factory function for implementations of a dependent operation,
// given a dependency specification:
type FactoryType<
Signatures extends GenSigs,
K extends (keyof Signatures) & string,
DS extends DepSpec<Signatures, (keyof DS & string)>
> = (dep: DepType<Signatures, DS>) => Signatures[K]
// The type of an implementation specification for an operation given its
// dependency specification: either directly the implementation if there
// are actually no dependencies, or a factory function and collection of
// dependency names otherwise:
type ImpType<
Signatures extends GenSigs,
K extends (keyof Signatures) & string,
DS extends DepSpec<Signatures, (keyof DS & string)>
> = DS extends null ? {implementation: Signatures[K]}
: {factory: FactoryType<Signatures, K, DS>, dependencies: DS}
// A collection of implementations for some operations of an ensemble of
// Signatures, matching a given collection of dependency specifications
type Implementations<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>,
Specs extends Specifications<Signatures, NeedKeys, NeedList>
> = {[K in NeedKeys]: ImpType<Signatures, K, Specs[K]>}
// The builder interface that lets us assemble narrowly-typed Implementations:
interface ImplementationBuilder<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>,
Specs extends Specifications<Signatures, NeedKeys, NeedList>
> {
independent<NewKeys extends (keyof Signatures) & string>(
independentImps: {[K in NewKeys]: Signatures[K]}
): ImplementationBuilder<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: never},
Specs & {[K in NewKeys]: null}
>
dependent<
RD extends RawDependencies, // Easier to infer
NewKeys extends (keyof Signatures) & string,
DepKeys extends string = keyof RD & string
>(
depSpec: RD,
imps: {
[K in NewKeys]:
FactoryType<Signatures, K, DepCheck<RD, Signatures>>
}
): ImplementationBuilder<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: DepKeys},
Specs & {[K in NewKeys]: DepCheck<RD, Signatures>}
>
ship(): Implementations<Signatures, NeedKeys, NeedList, Specs>
}
// And a function that actually provides the builder interface:
function impBuilder<
Signatures extends GenSigs,
NeedKeys extends keyof Signatures & string,
NeedList extends Record<NeedKeys, string>,
Specs extends Specifications<Signatures, NeedKeys, NeedList>
>(
sofar: Implementations<Signatures, NeedKeys, NeedList, Specs>
): ImplementationBuilder<Signatures, NeedKeys, NeedList, Specs> {
return {
independent<NewKeys extends (keyof Signatures) & string>(
imps: {[K in NewKeys]: Signatures[K]}) {
return impBuilder({
...sofar,
...Object.fromEntries(Object.keys(imps).map(k => [k, {
implementation: imps[k]
}]))
} as Implementations<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: never},
Specs & {[K in NewKeys]: null}
>)
},
dependent<
RD extends RawDependencies,
NewKeys extends (keyof Signatures) & string,
DepKeys extends string = keyof RD & string
>(
depSpec: RD,
imps: {
[K in NewKeys]:
FactoryType<Signatures, K, DepCheck<RD, Signatures, DepKeys>>
}
) {
return impBuilder({
...sofar,
...Object.fromEntries(Object.keys(imps).map(k => [k, {
factory: imps[k],
dependencies: depSpec
}]))
}) as unknown as ImplementationBuilder<
Signatures,
NeedKeys | NewKeys,
NeedList & {[K in NewKeys]: DepKeys},
Specs & {[K in NewKeys]: DepCheck<RD, Signatures, DepKeys>}
>
},
ship() {
return (sofar as
Implementations<Signatures, NeedKeys, NeedList, Specs>)
}
}
}
// A convenience function that gives you an implementation builder:
export function implementations<Signatures extends GenSigs>(
): ImplementationBuilder<Signatures, never, {}, {}> {
return impBuilder({})
}

7
src/core/type.ts Normal file
View file

@ -0,0 +1,7 @@
import type {TypeName} from '@/interfaces/type'
// Dummy implementation for now
export function joinTypes(a: TypeName, b: TypeName) {
if (a === b) return a
return 'any'
}