Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Type for referencing keys of another property on the same object

I have an object composed of Records mapping string keys to known types.

type Material = string;
type Mesh = { material: string };
interface Library {
  materials: Record<string, Material>;
  meshes: Record<string, Mesh>;
}

Some of those known types may need to reference a value from another record: for example a Mesh has a material and the value for that property must be a key of the materials record.

This is how I turned that constraint into types:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

type Material = string;
type Mesh<MaterialId> = {
  material: MaterialId;
}

interface Library<
  MaterialId extends string
> {
  materials: Record<MaterialId, Material>;
  meshes: Record<string, Mesh<MaterialId>>;
}

That works, and trying to set a material to a string that doesn’t exist on the materials record will cause an error:

function loadLibrary<T extends string>(lib: Library<T>) {}

// Not valid
loadLibrary({
  materials: {
    plastic: "./plastic.png",
    metal: "./metal.png"
  },
  meshes: {
    ball: { material: "plastic" },
    pipe: { material: "metal" },
    chair: { material: "wood" }
  }
});

However, the error I get here is that the property "wood" is missing on the materials record. The error I want is that "wood" is not a valid value for material because it doesn’t exist on the record.
The reason I want this is that if I try to reference a value that doesn’t exist, I want that reference to be marked as an error so I know to either add that type to the relevant record or use an existing one. It’s not useful to have material accept any string and trigger an error on the record, instead of the other way around.

loadLibrary({
  materials: { // << There's nothing wrong here, `materials` should be what defines the valid values for this type.
    plastic: "./plastic.png",
    metal: "./metal.png"
  },
  meshes: {
    ball: { material: "plastic" },
    pipe: { material: "metal" },
    chair: { material: "wood" } // << The error should be "wood", it's not a key of `materials`.
  }
});

Is this possible? Or can I not control which of the references to that generic type control the valid values for it?

Here’s a playground.

>Solution :

You can slightly change your generic constraint to achieve this:

interface Library<
  Materials extends Record<string, Material>
> {
  materials: Materials;
  meshes: Record<string, Mesh<keyof Materials>>;
}

function loadLibrary<T extends Record<string, Material>>(lib: Library<T>) {}

loadLibrary({
  materials: {
    plastic: "./plastic.png",
    metal: "./metal.png"
  },
  meshes: {
    ball: { material: "plastic" },
    pipe: { material: "metal" },
    chair: { material: "wood" }, // Type '"wood"' is not assignable to type '"plastic" | "metal"'.(2322)
  }
});

This works because we’re now using the generic to define materials only and deriving meshes from it, whereas before TypeScript would try to make both materials and meshes satisfy MaterialId extends string by making it a union of the values of materials and the keys of meshes.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading