1

I would like to publish an NPM package (written in Typescript) that looks like this:

const networks = { main: 1, test: 2, dev: 3 }

const resource = {
  foo: {
    [networks.main]: "hello",
    [networks.test]: "there",
    [networks.dev]: "friend"
  },
  bar: {
    [networks.main]: "how",
    [networks.test]: "are",
    [networks.dev]: "you"
  },
  baz: {
    [networks.main]: "have a",
    [networks.test]: "good",
    [networks.dev]: "day"
  }
}

And I want to make sure the user can set the network once and not have to worry about what network they are on:

const myMainResource = getResourceFor(networks.main)
const myTestResource = getResourceFor(networks.test)
const myDevResource = getResourceFor(networks.dev)

console.log(myMainResource.foo) // "hello"
console.log(myTestResource.foo) // "there"
console.log(myDevResource.foo) // "friend"

This way, instead of having to write resource.foo[networks.main] they can just write resource.foo and get the same thing.

The real object is actually a lot more nested, for example: alice.contracts.foo, bob.protocol.bar, etc. But the way the data is stored, the network is always at the leaf node of the tree.

I've already written the getResourceFor function and it works as expected by recursing through all the resources and replacing the networks (which is why the data is "generated" at run-time).

But I also want to have auto-complete work in the editor (using VS Code). And since the object is created dynamically at run-time, the editor can't really know the shape of the resulting JSON object.

What is the best way to approach this?

If you want to see the real code, it's here: https://github.com/studydefi/money-legos

adrianmcli
  • 1,956
  • 3
  • 21
  • 49
  • "*since the object is created dynamically at run-time*" - the object in your example code is not, `networks` is constant. How does your actual code look like? – Bergi Apr 10 '20 at 21:33
  • @Bergi I've added a link to the real code. When I say it's created dynamically at run-time, I am talking about how `getResourceFor` recurses over the entire data object and returns a completely new object. See this file: https://github.com/studydefi/money-legos/blob/master/src/legos.ts – adrianmcli Apr 10 '20 at 21:37
  • You might find some inspiration in [an answer I wrote](https://stackoverflow.com/a/60340543) that involved finding the paths to leaf nodes. – Scott Sauyet Apr 10 '20 at 21:37
  • @adrianmc Ok, but it should still be possible to make `networks` an `enum`, and type all the lookup objects by that using indexed types. – Bergi Apr 10 '20 at 21:43
  • 2
    @ScottSauyet I think the OP is less concerned about how to write the `getResourceFor` algorithm, but rather how to properly type it. – Bergi Apr 10 '20 at 21:44
  • @Bergi: you're probably right. I'm reading too fast. Since this seems mostly about Typescript, about which I intentionally know very little, I'll shut up now! – Scott Sauyet Apr 10 '20 at 21:56
  • @Bergi I have followed your suggestion but have ran into a bunch of errors. Perhaps I am not understanding you correctly? You can see what I did in this PR: https://github.com/studydefi/money-legos/pull/1 The error is also inside the PR. – adrianmcli Apr 10 '20 at 22:02
  • @adrianmc That's a weird error, given you are exporting `networks`. Maybe "private" means that you also need to export it from your entry point module, so that users of your library can refer to the enum? – Bergi Apr 11 '20 at 15:45
  • In any case, I don't think it doesn't matter much whether you use an `enum networks` or something more complicated with `keyof networks` - your data objects should be typed by referring to `networks`. – Bergi Apr 11 '20 at 15:47

1 Answers1

0

I ended up using this answer: Deep Omit with typescript

It's super messy, but hey at least it works. The result is here:

type Primitive =
  | string
  | Function
  | number
  | boolean
  | Symbol
  | undefined
  | null;

type MappingToChangeFrom = {
  address: {
    [x: number]: string;
  };
};

type MappingToChangeTo = {
  address: string;
};

type DeepOmitHelper<T> = {
  [P in keyof T]: T[P] extends infer TP //extra level of indirection needed to trigger homomorhic behavior // distribute over unions
    ? TP extends Primitive
      ? TP // leave primitives and functions alone
      : TP extends any[]
      ? DeepOmitArray<TP> // Array special handling
      : TP extends MappingToChangeFrom // IF type equals to { address: { [networkIds: number]: string } }
      ? Omit<TP, "address"> & MappingToChangeTo // Change to { address: string }
      : DeepOmit<TP>
    : never;
};

type DeepOmit<T> = T extends Primitive ? T : DeepOmitHelper<T>;

type DeepOmitArray<T extends any[]> = {
  [P in keyof T]: DeepOmit<T[P]>;
};

type RawLegos = typeof rawLegos;
type RawLegosWithoutNetworkId = DeepOmit<RawLegos>;

const isValidObject = (obj: unknown) => typeof obj === "object" && obj !== null;

// Recursively goes through each field, and changes the address value to the specific value
// i.e. compound.cDai.address[mainnet] = 0x...
//      becomes:
//      compound.cDai.address = 0x....
export const changeAddressValue = (
  networkId: number,
  immutableObj: RawLegos,
): RawLegosWithoutNetworkId => {
  let obj = immutableObj as any;

  // recursive base case, stop here
  if (!isValidObject(immutableObj)) {
    return obj;
  }

  // desctructure the object to create new reference
  obj = { ...immutableObj };
  // iterating over the object using for..in
  for (const key in obj) {
    if (Array.isArray(obj[key])) continue; // ignore arrays (e.g. ABIs)
    if (!isValidObject(obj[key])) continue; // ignore non-valid objects

    if (key === "address") {
      obj[key] = obj.address[networkId] || null;
    } else {
      obj[key] = changeAddressValue(networkId, obj[key]);
    }
  }
  return obj;
};
adrianmcli
  • 1,956
  • 3
  • 21
  • 49