Richard Lindhout
Richard Lindhout

Reputation: 2068

Autocomplete in return type Typescript based on input object

Update: thanks to https://stackoverflow.com/users/5575595/drag13 completed version can be found here: https://github.com/web-ridge/react-ridge-translations/blob/main/src/index.ts

I'm working on a translation library for React / React Native but I can't get the types to work.

https://github.com/web-ridge/react-ridge-translations

You can create a translation in the following way

// first describe which languages are allowed/required (Typescript)
type TranslationLanguages = {
    nl: string
    fr: string
    en: string
}

// create a translation object with your translations
export default const translate = createTranslations<TranslationLanguages>({
  homeScreen:{
    yesText: {
      nl: 'Ja',
      fr: 'Oui',
      be: 'Yes',
    },
    welcomeText: ({ firstName }: { firstName: string }) => ({
      nl: `Hoi ${firstName}`,
      fr: `Hello ${firstName}`,
      be: `Hello ${firstName}`,
    }),
  }
}, {
    language: 'nl',
    fallback: 'en',
})

The library changes the object to the following

{
  homeScreen:{
    yesText: 'Ja',
    welcomeText: ({ firstName }: { firstName: string }) => `Hoi ${firstName}`,
  }
}

In your component you will use the types in this way

  const {yesText,welcomeText} = translate.use().appScreen

It won't autocomplete the types.

Library code (simplified)

type val<T> = (...params: any[]) => T
type val1<T> = T

type Translations<T> = {
    [group: string]: {
        [key: string]:  val<T> | val1<T>
    },
}
type TranslationsObject<T> = {
    translations: Translations<string>,
    use: () => Translations<string>;
}
export function createTranslations<T>(t: Translations<T>): TranslationsObject<T> 

How can I let Typescript understand that it needs to autocomplete?

Upvotes: 2

Views: 2099

Answers (2)

Drag13
Drag13

Reputation: 5988

So, you want to allow TypeScript to define interface himself. In this case, you need to use more generics. Something like this:

type ValueOf<T> = T[keyof T]; 
type Translations<TGroup> = {
   [group in keyof TGroup]: { [key in keyof ValueOf<TGroup>]: ValueOf<TGroup>[key] }
}

type TranslationsObject<TGroup> = {
    translations: Translations<TGroup>,
    use: () => Translations<TGroup>;
}

export function createTranslations<TGroup>(data: TGroup): TranslationsObject<TGroup> { }


const test = createTranslations({
  homeScreen:{
    yesText: {
      nl: 'Ja',
      fr: 'Oui',
      be: 'Yes',
    },
    welcomeText: ({ firstName }: { firstName: string }) => ({
      nl: `Hoi ${firstName}`,
      fr: `Hello ${firstName}`,
      be: `Hello ${firstName}`,
    }),
  }
});

const x  = test.use().homeScreen.yesText

Type checking should work fine for now. Does this look good?

Updated with code from comment

Upvotes: 1

Drag13
Drag13

Reputation: 5988

Issue is here

type Translations<T> = {
    [group: string]: {
        [key: string]:  val<T> | val1<T>
    },
}

Because of the indexer declaration, TypeScript doesn't know anything about the inner structure. If you want to have more strong types, here is the option:

type val<T> = (...params: any[]) => T
type val1<T> = T
type TransltionGroup = 'homeScreen' | 'personalCabinet';
type Translations<T> = {
    [group in TransltionGroup]: {
        [key: string]:  val<T> | val1<T>
    }
}
type TranslationsObject<T> = {
    translations: Translations<string>,
    use: () => Translations<string>;
}
export function createTranslations<T>(t: Translations<T>): TranslationsObject<T> { }

const test = createTranslations({ homeScreen: { test: '4' }, personalCabinet: { test: '6' } });

const x  = test.use().personalCabinet

And of course, you may declare the type for the subgroup

Hope this helps!

Upvotes: 1

Related Questions