'Make id in FormattedMessage from react-intl inherit from a custom TypeScript interface to enable VS IntelliSense and type checking

Given that react-localization does not have date and number format and is heavily dependent on one developer we decided to switch to react-intl because it seems safer in the long run.

https://github.com/stefalda/react-localization/graphs/contributors

Our previous code looked like this:

localizationService.ts

import LocalizedStrings from 'react-localization';

import svSE from './languages/sv-SE';
import enUS from './languages/en-US';
import arSA from './languages/ar-SA';

export default new LocalizedStrings({
    svSE,
    enUS,
    arSA
});

ILanguageStrings.ts

export interface ILanguageStrings {
    appName: string
    narration: string
    language: string
}

en-US.ts

import { ILanguageStrings } from '../ILanguageStrings';

const language: ILanguageStrings = {
    appName: "Our App",
    narration: "Narration",
    language: "Language"
}

export default language;

Localization could then be imported and ILanguageStrings was visible via IntelliSense in Visual Studio and validated by TypeScript.

import localization from '../services/localizationService';

enter image description here

However using FormattedMessage from react-intl id is either string | number | undefined. We still use the language files so how can we make sure id is in ILanguageStrings without breaking the original type definitions from react-intl?

enter image description here

I tried with TypeScript Declaration Merging and Merging Interfaces but I could only add new members there and not change the id property. A "valid" string was not seen as correct either.

react-app-env.d.ts:

import * as reactIntl from 'react-intl';

declare module 'react-intl' {
    export interface MessageDescriptor {
        id?: ILanguageStrings;
        idTest: ILanguageStrings 
    }
}

enter image description here

https://github.com/microsoft/TypeScript/issues/10859

https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces



Solution 1:[1]

I had the same problem before when using react-intl with typescript. My solution is simply to create a wrapper component that provides the appropriate type for the id. The id type should be the keyof the language config object that has the most support.

Assuming the content of the file ./languages/en-US has something like this

{
  "AUTH.GENERAL.FORGOT_BUTTON": "Forgot Password",
  "AUTH.LOGIN.TITLE": "Login Account",
  "AUTH.FORGOT.TITLE": "Forgotten Password?",
  "AUTH.REGISTER.TITLE": "Sign Up",
  "AUTH.VALIDATION.INVALID": "{name} is not valid",
  "AUTH.VALIDATION.REQUIRED": "{name} is required",
  "AUTH.VALIDATION.NOT_FOUND": "The requested {name} is not found",
  "AUTH.VALIDATION.INVALID_LOGIN": "The login detail is incorrect",
  "AUTH.VALIDATION.REQUIRED_FIELD": "Required field",
  "AUTH.VALIDATION.INVALID_FIELD": "Field is not valid",
  "MENU.DASHBOARD": "Dashboard",
  "MENU.PRODUCT": "Product",
  "TOPBAR.GREETING": "Hi,",
  ...
}

I18nProvider.tsx

import React from "react";
import { IntlProvider } from "react-intl";
import svSE from './languages/sv-SE';
import enUS from './languages/en-US';
import arSA from './languages/ar-SA';

// In this example, english has the most support, so it has all the keys
export type IntlMessageID = keyof typeof enUS;

export default function I18nProvider({ children }) {
  return (
    <IntlProvider locale="en" messages={enMessages}>
      {children}
    </IntlProvider>
  );
}

FormattedMessage.tsx

import React from "react";
import { FormattedMessage as ReactFormattedMessage } from "react-intl";
import { IntlMessageID } from "./I18nProvider";

type FormattedMessageProps = {
  id?: IntlMessageID;
  defaultMessage?: string;
  values?: Record<string, React.ReactNode>;
  children?: () => React.ReactNode;
};

export default function FormattedMessage(props: FormattedMessageProps) {
  return <ReactFormattedMessage {...props} />;
}

Usage

import React from "react";
import I18nProvider from "./I18nProvider";
import FormattedMessage from "./FormattedMessage";

export default function App() {
  return (
    <I18nProvider>
      <div className="App">
        <FormattedMessage id="..." />
      </div>
    </I18nProvider>
  );
}

Here is the result

enter image description here

Live Demo

In the demo below, you can trigger IntelliSense in the editor by pressing Ctrl + Space

Edit React-Int - Message ID Type

Solution 2:[2]

My first thought was same as @NearHuscarl

However, I found that the document has a section about this:

https://formatjs.io/docs/react-intl#typing-message-ids-and-locale

You can use global declaration to augment the type of message id

declare global {
  namespace FormatjsIntl {
    interface Message {
      ids: keyof typeof messages
    }
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 ???