Home > Blockchain >  How to model nested enums in TypeScript?
How to model nested enums in TypeScript?

Time:03-23

I'm currently working on the CVSS v3.1 implementation in TypeScript. Here is the specification. The most interesting part for my question is probably Table 15: Base, Temporal and Environmental Vectors. Let's have a look at the Environmental metric group. Here we have the metrics Confidentiality Requirement, Integrity Requirement, etc. Each metric has possible values, e.g. Not Defined(X), High(H), Medium(M), and Low(L). I thought that's a pretty good use case for enums and records.

I looked at all examples I could find online but most examples are pretty simple and their value type is always the same. My value type depends on the key.

Here is what I currently have. I tried to keep it simple and use only two metrics.

enum EnvironmentalMetric {
  ConfidentialityRequirement = 'CR',
  IntegrityRequirement = 'IR',
}

// references EnvironmentalMetric.ConfidentialityRequirement (CR)
enum ConfidentialityRequirement {
  NotDefined = 'X',
  Low = 'L',
  Medium = 'M',
  High = 'H',
}

// references EnvironmentalMetric.IntegrityRequirement (IR)
enum IntegrityRequirement {
  NotDefined = 'X',
  Low = 'L',
  Medium = 'M',
  High = 'H',
}

In the end I'd like to have two loops, an outer loop over all metrics and an inner loop for each metric value. So I tried to define a nested record. For every metric value I need to have some information like a textual description and a numeric value. This numeric value is required afterwards to calculate a score. So accessing this value in a type safe manner is also required.

interface Information {
  text: string
  value: number
}

// can I improve this?
interface Metrics {
  text: string
  metrics:
    | Record<ConfidentialityRequirement, Information>
    | Record<IntegrityRequirement, Information>
}

const foo: Record<EnvironmentalMetric, Metrics> = {
  [EnvironmentalMetric.ConfidentialityRequirement]: {
    text: 'Confidentiality Requirement',
    metrics: {
      [ConfidentialityRequirement.NotDefined]: {
        text: 'Not Defined',
        value: 1,
      },
      [ConfidentialityRequirement.Low]: { text: 'Low', value: 1 },
      [ConfidentialityRequirement.Medium]: { text: 'Medium', value: 1 },
      [ConfidentialityRequirement.High]: { text: 'High', value: 1 },
    },
  },
  [EnvironmentalMetric.IntegrityRequirement]: {
      text: 'Integrity Requirement',
      metrics: {
          [IntegrityRequirement.NotDefined]: { text: 'Not Defined', value: 1 },
          [IntegrityRequirement.Low]: { text: 'Low', value: 1 },
          [IntegrityRequirement.Medium]: { text: 'Medium', value: 1 },
          [IntegrityRequirement.High]: { text: 'High', value: 1 },
      }
  }
}

// this should have type "Record<ConfidentialityRequirement, Information>"
// but has "Record<ConfidentialityRequirement, Information> | Record<IntegrityRequirement, Information>"
const a = better.CR.metrics

// this should have type "Record<IntegrityRequirement, Information>"
// but has "Record<ConfidentialityRequirement, Information> | Record<IntegrityRequirement, Information>"
const b = better.IR.metrics

My questions are:

  • Can I improve my Metrics interface so I don't have to use the union type?
  • Can I somehow make the "getters" type safe? So can I somehow say "whenever I get the value at key EnvironmentalMetric.ConfidentialityRequirement TypeScript knows it is of type Record<ConfidentialityRequirement, Information>".

I also tried using native Map, which works, but accessing values is quite annoying because every value can be undefined.

Here is a link to the playground.

Any ideas? Thank you very much!

CodePudding user response:

We can using a mapped type an a map of one enum to the other:

type MetricToRequirement = {
  [EnvironmentalMetric.ConfidentialityRequirement]: ConfidentialityRequirement;
  [EnvironmentalMetric.IntegrityRequirement]: IntegrityRequirement;
};

type Better = {
  [K in EnvironmentalMetric]: {
    text: string;
    metrics: Record<MetricToRequirement[K], Information>;
  };
};

You can liken this to a piece of (psuedo) code if it helps you understand:

let MetricToRequirement = {
  [EnvironmentalMetric.ConfidentialityRequirement]: ConfidentialityRequirement;
  [EnvironmentalMetric.IntegrityRequirement]: IntegrityRequirement;
};

let obj = {};

for each member of EnvironmentalMetric:
    obj[K] = {
        text: string;
        metrics: Record<MetricToRequirement[member], Information>;
    }

Playground

  • Related