Home > Enterprise >  Array of property names into properties
Array of property names into properties

Time:12-18

I've got an object where each key is a function that could be used by a "processor".

const fns = {
  foo: () => ({ some: "data", for: "foo" }),
  bar: () => ({ and: "data", for: "bar" }),
  baz: () => ({ baz: "also", is: "here" }),
};

Then I've got an interface describing the specification of a "processor":

interface NeedyProcessor {
  needed: Array<keyof typeof fns>;
  // Question 1: Can needsMet be typed more strongly?
  process: (needsMet: any) => any;
}

My goal is that needed should be an array with the function properties in fns that this processor needs to process.

So if needed is ["foo"] it means that processor will get an object needsMet that has a single property, foo with the value of foo from fns.

I didn't know how to type this properly, and the lack of typing makes this error go under the radar of Typescript:

const broken: NeedyProcessor = {
  needed: ["foo", "baz"],
  process: (needsMet) => ({
    fooResult: needsMet.foo(),
    // Question 1 cont: So that this call would give an error in the IDE
    barResult: needsMet.bar(), // Runtime error! bar won't exist
    bazResult: needsMet.baz(),
  }),
};

Just for completion and perhaps to clarify a bit more here is also an example without a runtime error:

const working: NeedyProcessor = {
  needed: ["bar"],
  process: (needsMet) => ({
    barResult: needsMet.bar(),
  }),
};

I will have a function similar to this one for making the call. It's important to only pass in the needed fns to process:

function callProcessor(spec: NeedyProcessor) {
  // Question 2: Could this any be typed more strongly? Not as important as question 1 though
  const needsMet: any = {}

  spec.needed.forEach(x => needsMet[x] = fns[x])

  return spec.process(needsMet);
}

One will work and not the other:

console.log("working", callProcessor(working));
console.log("broken", callProcessor(broken));

Playground Link

CodePudding user response:

I'm thinking you should use generics instead of an the needed property. Something like this:

const fns = {
  foo: () => ({ some: "data", for: "foo" }),
  bar: () => ({ and: "data", for: "bar" }),
  baz: () => ({ baz: "also", is: "here" }),
};

interface NeedyProcessor<T extends keyof typeof fns> {
  process: (needsMet: Pick<typeof fns, T>) => unknown;
}

const broken: NeedyProcessor<"foo" | "baz"> = {
  process: (needsMet) => ({
    fooResult: needsMet.foo(),
    barResult: needsMet.bar(), // Runtime error! bar won't exist
    bazResult: needsMet.baz(),
  }),
};

I'm sure there is a way to require the needed property to include all values in the union, but I can't come up with it now. Maybe infer can be used somehow to make it even easier?

=========

Edit:

I thought about it a bit more and came up with a solution that would keep the array of needed values in runtime.

type Needed = "foo" | "bar" | "baz"

const fns = {
  foo: () => ({ some: "data", for: "foo" }),
  bar: () => ({ and: "data", for: "bar" }),
  baz: () => ({ baz: "also", is: "here" }),
};

interface NeedyProcessor<T extends readonly Needed[]> {
  needed: T,
  // Instead of the return type any, 
  // you could use Record<string, ReturnType<typeof fns[T[number]]>> 
  // for the given "broken" example below.
  // Otherwise, I'd recommend using unknown instead of any
  process: (needsMet: Pick<typeof fns, T[number]>) => any; 
}

const broken: NeedyProcessor<["foo", "baz"] > = {
  needed: ["foo", "baz"] ,
  process: (needsMet) => ({
    fooResult: needsMet.foo(),
    barResult: needsMet.bar(), // Runtime error! bar won't exist
    bazResult: needsMet.baz(),
  }),
};

The only drawback is that the array of needed has to be sent as a generic type as well as to the needed field. If you prefer, you can create an as const array and use it in both the generic type and the needed value, like this:

const neededBroken = ["foo", "baz"] as const
const broken: NeedyProcessor<typeof neededBroken> = {
  needed: neededBroken,
  process: (needsMet) => ({
    fooResult: needsMet.foo(),
    barResult: needsMet.bar(), // Runtime error! bar won't exist
    bazResult: needsMet.baz(),
  }),
};

CodePudding user response:

There isn't a good specific type that would meet your needs. Conceptually NeedyProcessor could be a union of all acceptable input types, like this:

type NuttyProfessor =
  { needed: []; process: (needsMet: Pick<Fns, never>) => any; } |
  { needed: ["baz"]; process: (needsMet: Pick<Fns, "baz">) => any; } |
  { needed: ["bar"]; process: (needsMet: Pick<Fns, "bar">) => any; } |
  { needed: ["bar", "baz"]; process: (needsMet: Pick<Fns, "bar" | "baz">) => any; } |
  { needed: ["baz", "bar"]; process: (needsMet: Pick<Fns, "bar" | "baz">) => any; } |
  { needed: ["foo"]; process: (needsMet: Pick<Fns, "foo">) => any; } |
  { needed: ["foo", "baz"]; process: (needsMet: Pick<Fns, "foo" | "baz">) => any; } |
  { needed: ["foo", "bar"]; process: (needsMet: Pick<Fns, "foo" | "bar">) => any; } |
  { needed: ["foo", "bar", "baz"]; process: (needsMet: Pick<Fns, "foo" | "bar" | "baz">) => any; } |
  { needed: ["foo", "baz", "bar"]; process: (needsMet: Pick<Fns, "foo" | "bar" | "baz">) => any; } |
  { needed: ["baz", "foo"]; process: (needsMet: Pick<Fns, "foo" | "baz">) => any; } |
  { needed: ["bar", "foo"]; process: (needsMet: Pick<Fns, "foo" | "bar">) => any; } |
  { needed: ["bar", "foo", "baz"]; process: (needsMet: Pick<Fns, "foo" | "bar" | "baz">) => any; } |
  { needed: ["bar", "baz", "foo"]; process: (needsMet: Pick<Fns, "foo" | "bar" | "baz">) => any; } |
  { needed: ["baz", "foo", "bar"]; process: (needsMet: Pick<Fns, "foo" | "bar" | "baz">) => any; } |
  { needed: ["baz", "bar", "foo"]; process: (needsMet: Pick<Fns, "foo" | "bar" | "baz">) => any; };

But that doesn't scale well at all with the size of fns, and because it's not a discriminated union it wouldn't even work the way you want... the process callback parameter cannot be contextually typed:

const shouldWorkButDoesNot: NuttyProfessor = {
  needed: ["bar"],
  process: (needsMet) => ({ // error!
    // ---> ~~~~~~~~ implicit any            
  • Related