How to model nested enums in TypeScript?

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.

Any ideas? Thank you very much!