1

I am writing a module to display information to the user about a data structure. I am trying to make it as generic as possible, so rather than writing specific code for every possible kind of data structure, I am iterating over the structure to be displayed at runtime, and getting a text label and a formatting function for that field.

To achieve this, I've created the following types:

// base interface for data to be displayed in a panel
interface BaseEntity {
  [key: string]: Formattable;
}
// an example entity whose data we want to display
interface EntityInfo extends BaseEntity {
  [key: string]: Formattable;
  uniqueId: number;
  entityName: string;
  entityPosition: Vector3;
}

// types that we can have formatters for
type Formattable = string | number | Vector3;

// generic type for formatters
type ValueFormatter<T> = (value: T) => string;

// label is the text label to display, formatter is the function to format the value with
type DisplayInfo<T> = { label: string, formatter: ValueFormatter<T extends Formattable ? T : never> };

// a map of fields on a BaseEntity to the label and formatter for each field
type PanelDataDescriptor<T> = Map<string, T extends Formattable ? DisplayInfo<T> : never>;

Here is some sample display code:

 function displayText(id: number, text: string) {
    console.log(`id: ${id} ${text}`);
  }
  function displayEntityInfo(dataDescriptor: PanelDataDescriptor<Formattable>, data: BaseEntity) {
    dataDescriptor.forEach((displayInfo, key: string) => {
      const fieldData: Formattable = data[key];
      displayText(0, displayInfo.label);
      displayText(1, displayInfo.formatter(fieldData));
    });
  }

Now we can set up some structures will describe what we want to display:

const myDescriptor: PanelDataDescriptor<Formattable> = new Map([
  ["uniqueId", { label: "UniqueId", formatter: formatNumber }],
  ["entityName", { label: "Name", formatter: formatString }],
  ["entityPosition", { label: "Scenario Pos", formatter: formatVector }],
]);
const ent: EntityInfo = {
  uniqueId: 0,
  entityName: "Guybrush",
  entityPosition: { x: 0, y: 1, z: 2 }
}
displayEntityInfo(myDescriptor, ent);

My problem is this line in displayEntityInfo():

  displayText(1, displayInfo.formatter(fieldData));

causes this error:

Argument of type 'Formattable' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.

Why is the 'value' parameter of formatter() of type 'never'? We have a version of formatter() for each type in Formattable (i.e. string | number | Vector3)

Even though the typescript compiler creates the error, the code actually works: Typescript Playground with all the code

TimF
  • 151
  • 1
  • 2
  • 8
  • 2
    Nothing here is stopping you from [calling](//tsplay.dev/m3aDqw) `displayEntityInfo()` with two completely uncorrelated values like `displayEntityInfo(myDescriptor, {a: 1, b: ""});` which would have runtime errors in exactly the place you're being warned about. I think before I could proceed with how to help, you'd need to make sure the rest of your code is type safe. Otherwise you might as well use a [type assertion](//tsplay.dev/wX2xLm) if you're not worried about the problem. But you can't begin to convince the compiler that `displayInfo.formatter` can accept `data[key]` if it isn't true. – jcalz Dec 09 '21 at 02:22
  • 1
    For example, [this code](https://tsplay.dev/wOzQdW) is what I happen if I refactor your example significantly to try to ensure some type safety. `Map` is not a great structure to keep track of type relationships between string keys and value; we have plain objects for that (although [you *could* do it in TS with enough work](https://stackoverflow.com/q/54907009/2887218)). Does that help? – jcalz Dec 09 '21 at 02:26
  • I'm not quite sure how to proceed with an answer, since I've got a significant amount of changes that needed to be made and I think if I wrote something up it would be ten pages of text explaining it. Is it possible to pare the whole question down into something a little more minimal? Or maybe I should just answer with the demonstration that what you're doing is not type safe and the compiler is right to complain? What can we do to limit the scope here? – jcalz Dec 09 '21 at 02:27
  • I was typing up an answer around, why it's not type safe because the last question asked was `Why is the 'value' parameter of formatter() of type 'never'?`. Because I would honestly be too much work to try and refactor and explain each step. OP can think of their problem domain and see how they can fit everything in. The example that you gave of one possible refactor is good so maybe just including that should be good enough. – zecuria Dec 09 '21 at 02:34

0 Answers0