In a recent project, I ran into a problem where I needed to validate an object whose keys were dynamically defined by a constant map and enforce that at least one key had a valid value.
Challenge
We have a MetadataMap object that defines the valid keys and their corresponding types:
<code class="language-typescript">const MetadataMap = { userId: Number, utmSource: String, utmMedium: String, utmCampaign: String, } as const;</code>
According to this mapping, we need:
However, TypeScript enforces static typing at compile time, while Yup handles runtime verification.
Step 1: Generate interface
To generate a TypeScript interface from a MetadataMap, we use keyof
and map types. Here's how we define it:
<code class="language-typescript">type Metadata = { [K in keyof typeof MetadataMap]: typeof MetadataMap[K] extends NumberConstructor ? number : string; };</code>
This approach ensures that any updates to the MetadataMap are automatically reflected in the Metadata interface. For example:
// Generated Metadata interface:
<code class="language-typescript">interface Metadata { userId?: number; utmSource?: string; utmMedium?: string; utmCampaign?: string; }</code>
Step 2: Dynamically generate Yup pattern
We need to dynamically create a Yup schema that matches the keys and types in the MetadataMap. Using Object.keys
and reduce
, we map each key to its corresponding Yup validator:
<code class="language-typescript">const metadataSchema = Yup.object( Object.keys(MetadataMap).reduce((schema, key) => { const type = MetadataMap[key as keyof typeof MetadataMap]; if (type === Number) { schema[key] = Yup.number().optional(); } else if (type === String) { schema[key] = Yup.string().optional(); } return schema; }, {} as Record<string, any>) );</code>
This approach eliminates hardcoding and ensures that changes in the MetadataMap are reflected in the schema without the need for manual updates.
Step 3: Add the "at least one key" rule
The next challenge is to ensure that at least one key in the object has a defined value. We added a .test
method in Yup mode:
<code class="language-typescript">metadataSchema.test( "at-least-one-key", "Metadata must have at least one valid key.", (value) => { if (!value || typeof value !== "object") return false; const validKeys = Object.keys(MetadataMap) as (keyof typeof MetadataMap)[]; return validKeys.some((key) => key in value && value[key] !== undefined); } );</code>
This logic:
Results Here's how the final mode behaves:
<code class="language-typescript">const exampleMetadata = { userId: undefined, utmSource: "google", extraField: "invalid", // 此键被忽略。 }; metadataSchema .validate(exampleMetadata) .then(() => console.log("Validation succeeded")) .catch((err) => console.error("Validation failed:", err.errors));</code>
In this example, the validation succeeds because utmSource is a valid key with a non-undefined value, even though userId is undefined and extraField is not part of the MetadataMap.
The above is the detailed content of Dynamically Generating Interfaces and Validation Schemas in TypeScript with Yup. For more information, please follow other related articles on the PHP Chinese website!