🏳️‍⚧️ and I make good posts sometimes!

pfp by me!
banner by https://twitter.com/spect_ion


fediverse
‪@PolyWolf@social.treehouse.systems‬
bluesky
@wolf.girl.technology

i'm refactoring code to make some better abstractions. partway thru, i end up with a thing that looks like

interface IFoo {
  readonly id: string;
  foo: () => boolean;
  bar: (x: number) => () => void;
}

class FooDispatcher {
  private map = new Map<string, IFoo>();
  public constructor(fooList: IFoo[]) {
    fooList.forEach((foo) => this.map.set(foo.id, foo));
  }

  public foo(id: string) {
    return this.map.get(id)?.foo() ?? false;
  }

  public bar(id: string, x: number) {
    return this.map.get(id)?.bar(x) ?? (() => {});
  }
}

my eye twitches. i daydream about writing some awesomely complicated metaprogramming solution in Haskell. i sigh, save & close the file, and wait an entire minute for Typescript to typecheck everything during the commit hook.


You must log in to comment.

in reply to @PolyWolf's post:

to borrow some imaginary Rust syntax for a sec, because I do not know how other languages might abstract over fields:

// a typeclass is a that traits conform to,
// similar to how traits are things that types conform to
typeclass Defaultable {
  // read: every `&self` function in the trait must return a type that implements `Default`
  ${method:ident}(&self, $(${arg:ident}: ${argty:ty})*) -> impl Default;
}

// read: T is a trait that conforms to the typeclass Defaultable
struct Dispatcher<T conforms Defaultable> {
  map: HashMap<String, Box<dyn T>>,
}

impl<T conforms Defaultable> for Dispatcher<T> {
  // read: for all the methods that T declares and Defaultable matches against
  with T as Defaultable {
    ${method}(&self, id: &str, $(${arg:ident}: ${argty:ty})*) -> ReturnType<T::${method}> {
      self.map.get(id).map(T::${method}).or_else(|| Default::default())
    }
  }
}

now obviously there are a lot of ergonomic concerns that would have to be ironed out if this were to actually be put into Rust, but that's the basic gist of it

it's been a while, but i think you could do something similar in typescript, if you're willing to (ab)use enough of the dynamicism at your disposal. sketch:

interface IFoo {
  readonly id: string;
  foo: () => boolean;
  bar: (x: number) => () => void;
}

const FooDispatcher = mkDispatcher<IFoo>(
  // Type name
  'Foo',
  // Default values
  {
    foo: false,
    bar: x => {},
  }
);

function mkDispatcher
  <T extends { id: Id }, Id = string>
  (typeName: string, defaults: DefaultsFor<T>): DispatcherFor<T>
{
  const Dispatcher: any = class {
    constructor(items: T[]) {
      this.map = new Map<Id, T>();
      for (const item of items) {
        this.map.set(item.id, item);
      }
    }
  }

  for (const [fieldName, defaultValue] of Object.values(defaults)) {
    Dispatcher.prototype[fieldName] = function(id: Id, ...args: any) {
      return this.map.get(id)?.[fieldName]?.(...args) ?? defaultValue;
    }
  }

  Dispatcher.name = `${typeName}Dispatcher`;
  return Dispatcher as DispatcherFor<T>;
}

type DefaultsFor<T> = ...
type DispatcherFor<T> = ...

this feels like an use case for mapped types and some abuse of Proxy({}, {get() {}}) ... though that probably wouldn't pass code review unless it's a workplace where people like that kinda thing. and it'd probably be kind of bad for performance because the public foo(id: string) etc functions would be made over and over at runtime