typescripttype-definition

A type-definition rule to detect missing properties in repeating objects (e.g. multi language fields)


Imagine I have a type definition like:

export type TextMessagesType = {
  [language: string]: {
    [placeholder: string]: string;
  };
};

That would fit in this case:

export const TextMessages: TextMessagesType = {
  en: {
    noText: 'No Texts available!',
  },
};

Now, if I want to add new languages and new properties like:

export const TextMessages: TextMessagesType = {
  en: {
    noText: 'No Texts available!',
    welcome: 'You are welcome'
  },
  de: {
    noText: 'Keine weiteren Texte vorhanden!',
    // welcome missing
  },
};

I would like to ensure that the de-object has exactly the same properties as the en-object. The IDE should recognize the missing properties (e.g., welcome) due to type-definitions.

Can I do that with the assistance of typescript type definition rules? If yes, how?

EDIT: Excuse me, I think, an important information is missing:

The recognizing-mechanism should work dependent on existing properties in each object. Imagine de object has a property xy and it is missing in the en object and vice versa. If a language object gets a new property it should be marked in all other language objects as a missing property.


Solution

  • We can use a union type of strings to achieve this, with the in keyword on the type of the key:

    type RequiredLanguageFields = 'welcome'
      | 'noText';
    
    type TextMessagesType = {
      [language: string]: {
        [placeholder in RequiredLanguageFields]: string;
      };
    };
    
    const TextMessages: TextMessagesType = {
      en: {
        noText: 'No Texts available!',
        welcome: 'You are welcome'
      },
      de: {   // type error on this line
        noText: 'Keine weiteren Texte vorhanden!',
        // welcome missing
      },
    };
    

    Property 'welcome' is missing in type '{ noText: string; }' but required in type '{ welcome: string; noText: string; }'.(2741)

    It's a slight bit of extra work in that you need to define the required fields before you add them to the object. Alternatively you could have some master translation object and use the keys of that to define the required keys of the others:

    const enStrings = {
      noText: 'No Texts available!',
      welcome: 'You are welcome',
    };
    
    type TextMessagesType = {
      [language: string]: {
        [placeholder in keyof typeof enStrings]: string;
      };
    };
    

    Based on the edit to your question, I'll attempt to describe why I believe it won't be possible to create a type in the way you're looking to do it.

    Now we're saying that all values must be objects of the same type - they must all have the same properties and all of those properties must be strings. But what is that type? We might define some interface which takes a generic:

    interface ITextMessagesType<T> {
      [language: string]: {
        [placeholder in keyof T]: string;
      };
    };
    
    const TextMessages: ITextMessagesType = {    // error here as we have no passed in the type for the generic `T`
      en: {
        noText: 'No Texts available!',
        welcome: 'You are welcome'
      },
      de: {   // type error on this line
        noText: 'Keine weiteren Texte vorhanden!',
        // welcome missing
      },
    };
    

    We still need to define what that generic is; we're back to the problem you have with the original example I gave above - you need to define the keys before defining the object.

    It's a little easier in a function, as we can infer the type from the object passed in. But then we're onto the next issue; which object is treated as the required type? Take the following code as an example:

    const test = <T>(x: { [key: string]: { [key in keyof T]: string } }) => true;
    
    const x = test({
      en: {
        noText: 'No Texts available!',
        welcome: 'You are welcome',     // now we get a type error here
      },
      de: {
        noText: 'Keine weiteren Texte vorhanden!',
        // welcome missing
      },
    })
    

    The error that we get is:

    Type '{ noText: string; welcome: string; }' is not assignable to type '{ noText: string; }'. Object literal may only specify known properties, and 'welcome' does not exist in type '{ noText: string; }'.(2322)

    Here Typescript has determined that the value at de is the 'master' type - and as such in gives an error where we've tried to define the welcome key on en.

    As such I don't believe you'll be able to get what you're asking for - hopefully someone will come in and prove me wrong.