FullScreenShenanigans / EightBittr

Bare-bones, highly modular game engine for 2D 8-bit games.
MIT License
78 stars 10 forks source link

Generate ObjectMakr generic types automatically #260

Open JoshuaKGoldberg opened 3 years ago

JoshuaKGoldberg commented 3 years ago

Overview

Hi everyone from TSConf! 😄

My pinned tweet with slides links

Right now ObjectMakr requires manually defining interfaces alongside its inheritance and properties map. The actual way it works is slightly less readable from what I put in the slides but generally works the same.

const objectMaker = new ObjectMakr({
    inheritance: {
        Solid: {
             Block: {},
        },
    },
    properties: {
        Block: {
            photo: "Question Mark",
        },
        Solid: {
            size: 8,
        },
    },
});

interface Solid {
    size: 8;
}

interface Block extends Solid {
    photo: string;
}

const block = objectMaker.make<Block>("Block");
const solid = objectMaker.make<Solid>("Solid");

I'd really like to not have to write out and update those manual type changes. Now that TypeScript has conditional and recursive types we really should be able to figure this all out in the type system.

const objectMaker = new ObjectMakr({
    inheritance: {
        Solid: {
             Block: {},
        },
    },
    properties: {
        Block: {
            photo: "Question Mark",
        },
        Solid: {
            size: 8,
        },
    },
});

// { size: number; photo: string; }
const block = objectMaker.make("Block");

// { size: number; }
const solid = objectMaker.make("Solid");

As a fun little challenge: make ObjectMakr recognize these types!

Here's a starter snippet working for a simplified version of inheritance...

Affected Packages

ObjectMakr

shicks commented 3 years ago

How about something like this:

interface Template {
    inheritance: object;
    properties: object;
}

declare class ObjectMakr<T extends Template> {
    constructor(template: T);
    make<S extends keyof T["properties"]>(pattern: S): Output<T, S>;
}

declare function assert<T>(x: T): void;

const o = new ObjectMakr({
    inheritance: {
        Solid: {
            Block: {},
        },
    },
    properties: {
        Block: {photo: 'qmark'},
        Solid: {size: 8},
    },
} as const);
const b = o.make('Block');
const s = o.make('Solid');
assert<8>(s.size);
assert<'qmark'>(s.photo);
assert<'qmark'>(b.photo);

type Output<T extends Template, S extends keyof T["properties"],
            SS = Supers<T, S>,
            TT = Transpose<T["properties"]>> = OmitNevers<{
    [K in keyof TT]: SS extends keyof TT[K] ? TT[K][SS] : never;
}>;
type Supers<T extends Template, S, Acc = S> =
    S extends keyof T["inheritance"] ?
        Supers<T, keyof T["inheritance"][S], Acc | keyof T["inheritance"][S]> :
        Acc;
type Transpose<T> = {
  [J in Inner<T>]: {[K in keyof T]: T[K] extends {[_ in J]: infer U} ? U : never}
};
type Inner<T> = Keys<T[keyof T]>;
type Keys<T> = T extends object ? keyof T : never;
type OmitNevers<T> = Omit<T, Nevers<T>>;
type Nevers<T> = ({[P in keyof T]: T[P] extends never ? P : never })[keyof T];
JoshuaKGoldberg commented 3 years ago

@shicks I love it, and so soon after TSConf! Very nicely done! 👏

This looks great. If you'd like to send a PR I'd be happy to review it and try it out against what's in this repo. Otherwise no worries, I'll send one soon. Thanks either way!