EN VI

Typescript - Type for referencing keys of another property on the same object?

2024-03-09 23:00:06
How to Typescript - 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:

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.

Answer

Login


Forgot Your Password?

Create Account


Lost your password? Please enter your email address. You will receive a link to create a new password.

Reset Password

Back to login