TypeScript中基于另一属性的泛型映射类型:如何确保ScriptElement的type与data类型匹配
ScriptElement The core issue here is that your original ScriptElement interface doesn't link the type property to the data property—TypeScript sees data as a union of all possible element data types, regardless of what type is set to. To fix this while keeping your elements array flexible (allowing mixed element types), you need to use a discriminated union type instead of a generic interface.
Step-by-Step Fix
You can reuse your existing type definitions to build a clean, type-safe union that enforces matching between type and data:
enum ElementType { Text = 'text', Rating = 'rating', Picture = 'picture' } type TextData = { title: string; description: string; boldTitle: boolean; }; type RatingData = | { question: string; type: 'number'; scale: 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; minLabel: string; maxLabel: string; } | { question: string; type: 'star' | 'emoji'; scale: 2 | 3 | 4 | 5; minLabel: string; maxLabel: string; }; type PictureData = { url: string; }; // Keep your existing ElementData utility type type ElementData<Type extends ElementType> = Type extends ElementType.Text ? TextData : Type extends ElementType.Rating ? RatingData : Type extends ElementType.Picture ? PictureData : Record<string, never>; // Rewrite ScriptElement as a discriminated union type ScriptElement = { [Type in ElementType]: { name: string; type: Type; data: ElementData<Type>; }; }[ElementType]; // Extract the union of all mapped element types // ElementCategory stays unchanged (now accepts mixed element types) type ElementCategory = { name: string; elements: ScriptElement[]; }; // Default data remains valid and type-aligned const defaultElementsData: { [Type in ElementType]: ElementData<Type> } = { [ElementType.Text]: { title: '', description: '', boldTitle: false, }, [ElementType.Rating]: { question: '', type: 'number', scale: 10, minLabel: '', maxLabel: '', }, [ElementType.Picture]: { url: '', }, }; // Now TypeScript enforces type-data matching automatically! const ContentElementsCategory: ElementCategory = { name: 'Content', elements: [ { name: 'Text', type: ElementType.Text, data: defaultElementsData[ElementType.Text], }, { name: 'Picture', type: ElementType.Picture, data: defaultElementsData[ElementType.Picture], }, { name: 'Rating', type: ElementType.Rating, data: defaultElementsData[ElementType.Rating], }, // Uncomment this line to see the error: data doesn't match the Text type // { name: 'Invalid Element', type: ElementType.Text, data: defaultElementsData[ElementType.Picture], }, ], };
Why This Works
- The
ScriptElementtype is built by mapping eachElementTypeto a specific object wheretypeis fixed to that enum value, anddatais the correspondingElementData<Type>. Indexing into this mapped type with[ElementType]converts it into a union of all valid element types. - This is a discriminated union: TypeScript uses the
typeproperty (the discriminant) to automatically narrow down the alloweddatatype. Hover over anydataproperty in yourelementsarray now, and you'll see it correctly shows the matching data shape for that element'stype. - Unlike a generic interface approach, this union lets your
elementsarray contain mixed element types (Text, Rating, Picture) without forcing you to lock the array to a single generic type.
What Was Wrong With the Original Code?
Your original ScriptElement interface had:
interface ScriptElement { name: string; type: ElementType; data: ElementsData[ElementType]; }
Here, data is a union of all possible data types (TextData | RatingData | PictureData), with no enforced link to the type property. TypeScript couldn't infer that when type is ElementType.Text, data should only be TextData—it allowed any data type regardless of the type value.
内容的提问来源于stack exchange,提问作者GoncaloNGT

