ThomasReggi
ThomasReggi

Reputation: 59345

Reducing typing in class with special methods

I am looking for a way to reduce redundancy in my typescript typing. I am creating a specific type of class where:

Here's a working example of a class:

interface Wood {}
interface Chair {}
interface DiningRoom {}
interface LivingRoom {}
interface Nail {}

class House {

    constructor () {

    }

    wood (i: {tree: string}): Wood {
        return {}
    }

    nails (i: {nails: number}): Nail[] {
        return [{}]
    }

    chair (i: {wood: Wood, nails: Nail[]} 
        & Parameters<House['wood']>[0]
        & Parameters<House['nails']>[0]    
    ): Chair {
        return {}
    }

    diningRoom (i: {chair: Chair } & Parameters<House['chair']>[0]): DiningRoom {
        return {}
    }

    livingRoom (i: {chair: Chair } & Parameters<House['chair']>[0]): LivingRoom {
        return {}
    }

}

I am looking for either a abstract class or generic type that would make it less redundant to write & Parameters<House['wood']>[0]. Ideally an abstract class that would be able to see i: {chair: Chair} as the parameter and know to add & Parameters<House['chair']>[0]

Upvotes: 0

Views: 26

Answers (1)

jcalz
jcalz

Reputation: 327624

I am not sure if I understand what you're doing, and I have no idea if it's possible to do anything with inheritance that would make it possible to have subclass method parameters intersect with things automatically. As far as I can tell, the generic type corresponding to your parameters looks something like this:

type AllKeys<T> = T extends any ? keyof T : never;
type MakeParam<H extends Record<K, (x: any) => any>, K extends keyof any> =
  { [Q in K]: Parameters<H[Q]>[0] }[K] extends infer L ?
  { [Q in AllKeys<L>]: Extract<L, Record<Q, any>>[Q] } extends infer M ?
  { [P in K | keyof M]:
    (P extends keyof M ? M[P] : unknown) &
    (P extends K ? ReturnType<H[P]> : unknown)
  } : never : never;

You use it like this:

type Test = MakeParam<House, "wood">;
/* type Test = {
    wood: Wood;
    tree: string;
} */

or for multiple keys, as a union like this:

type Test2 = MakeParam<House, "wood" | "nails">;
/* type Test2 = {
    wood: Wood;
    nails: number & Nail[];
    tree: string;
} */

Notice that number & Nail[]. That's weird and useless, but is exactly what your current code requires, so there you go. Presumably the object you pass into the nails() method should not have a key named nails. Duplicates will be intersected; use at your own risk.

Then you can refactor your House definition like this:

class House {

  constructor() {
  }

  wood(i: { tree: string }): Wood {
    return {}
  }

  nails(i: { nails: number }): Nail[] {
    return [{}]
  }

  chair(i: MakeParam<House, "wood" | "nails">): Chair {
    return {}
  }

  diningRoom(i: MakeParam<House, "chair">): DiningRoom {
    return {}
  }

  livingRoom(i: MakeParam<House, "chair">): LivingRoom {
    return {}
  }

}

And it works the same as your given code, I think.


I suppose you want an explanation for MakeParam<H, K>. That could get really wordy, so I'm just going to give a sketch:

  • { [Q in K]: Parameters<H[Q]>[0] }[K] extends infer L ? ... : never

    This defines a type L which is the union of all the parameters for the methods named in the K union. When H is House and K is "wood" | "nails", you get { tree: string } | { nails: number }.

  • { [Q in AllKeys<L>]: Extract<L, Record<Q, any>>[Q] } extends infer M ? ... : never

    This defines a type M which takes L and turns the union into an "intersection" by extracting all the properties from all union pieces. When H is House and K is "wood" | "nails", you get { tree: string, nails: number }.

  • { [P in K | keyof M]: (P extends keyof M ? M[P] : unknown) & (P extends K ? ReturnType<H[P]> : unknown) }

    This makes a new object with all the properties from M, and also adds properties with names in K and types as the return type of the methods named by K. Generally each property name should only appear in either K or keyof M, and if I knew that for sure I'd have done something like M[P] | ReturnType<H[P]> (with some keyof checks to make that compiler), but your nails definition gave me pause and I had to make sure it behaved the same as your current thing, with the fun number & Nail[] type coming out.


Okay, hope that helps. Good luck!

Link to code

Upvotes: 1

Related Questions