Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to have the translationKey strongly typed? #721

Open
opolo opened this issue Nov 5, 2021 · 32 comments
Open

Is there a way to have the translationKey strongly typed? #721

opolo opened this issue Nov 5, 2021 · 32 comments
Assignees
Labels
enhancement New feature or request

Comments

@opolo
Copy link

opolo commented Nov 5, 2021

Hi there!

Thanks for a great library :)

Question regarding Typescript:
We have a Typescript solution using this library. However, we have not been able to find a way to make the translation-key provided to t() be strongly typed. The type of t seems to be t(string) no matter what we do.

Is it possible to make the input to the t() function strongly typed based on our json files with the localizations? It would help us a lot in avoiding typos.

Thanks! :)

NB. We tried downloading the complex typescript example, but that also seemed to allow any string as input to the t-function.

@ajmnz
Copy link
Contributor

ajmnz commented Nov 11, 2021

I solved it using the following custom hook

// util-types.ts

type Join<S1, S2> = S1 extends string
  ? S2 extends string
    ? `${S1}.${S2}`
    : never
  : never;

export type Paths<T> = {
  [K in keyof T]: T[K] extends Record<string, unknown>
    ? Join<K, Paths<T[K]>>
    : K;
}[keyof T];
// useTypeSafeTranslation.ts

import useTranslation from "next-translate/useTranslation";
import { TranslationQuery } from "next-translate";
import { Paths } from "../types/util-types";

import common from "../../locales/es-es/common.json";
import home from "../../locales/es-es/home.json";
import catalog from "../../locales/es-es/catalog.json";
import auth from "../../locales/es-es/auth.json";

export type TranslationKeys = {
  common: Paths<typeof common>;
  home: Paths<typeof home>;
  catalog: Paths<typeof catalog>;
  auth: Paths<typeof auth>;
};

export const useTypeSafeTranslation = <T extends keyof TranslationKeys>(
  ns: T
) => {
  const { t, lang } = useTranslation(ns);

  return {
    t: (
      s: TranslationKeys[T],
      q?: TranslationQuery,
      o?: {
        returnObjects?: boolean;
        fallback?: string | string[];
        default?: string;
      }
    ) => t(s, q, o),
    lang,
  };
};

And then you just pass the namespace to the hook, and you have a type safe t function

import React from "react";
import { useTypeSafeTranslation } from "./useTypeSafeTranslation";

interface TestComponentProps {}

export const TestComponent: React.FC<TestComponentProps> = () => {
  const { t } = useTypeSafeTranslation("common");

  return <>{t("footer.legal.paymentMethods")}</>;
}
;

Screenshot 2021-11-11 at 13 31 04

Adapted from https://github.com/benawad/dogehouse

@osdiab
Copy link

osdiab commented Nov 12, 2021

Here's my simplified version FYI, that just patches t and adds a minimum amount of code:

import type { I18n, Translate } from "next-translate";
import useTranslation from "next-translate/useTranslation";

import type { TranslationsKeys } from "src/utility/i18n/available-translations";

type Tail<T> = T extends [unknown, ...infer Rest] ? Rest : never;

export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
  extends Omit<I18n, "t"> {
  t: (
    key: TranslationsKeys[Namespace],
    ...rest: Tail<Parameters<Translate>>
  ) => string;
}

export function useTypeSafeTranslation<
  Namespace extends keyof TranslationsKeys
>(namespace: Namespace): TypeSafeTranslate<Namespace> {
  return useTranslation(namespace);
}

And I also just use import type for all the translations to be extra sure they don't accidentally get bundled (tree shaking has always been a little finnicky for me). I then added a lint rule with ESLint to error whenever users in my codebase try to use useTranslation directly, which if they really need to, can disable with an ESLint disable comment. Working on an equivalent helper for Trans.

@osdiab
Copy link

osdiab commented Nov 12, 2021

Here's a helper for Trans:

import UnsafeTrans from "next-translate/Trans";
import type { TransProps as UnsafeTransProps } from "next-translate";

import type { TranslationsKeys } from "src/utility/i18n/available-translations";

export interface TransProps<Namespace extends keyof TranslationsKeys>
  extends Omit<UnsafeTransProps, "i18nKey"> {
  i18nKey: `${Namespace}:${TranslationsKeys[Namespace]}`;
}

export function Trans<Namespace extends keyof TranslationsKeys>(
  props: TransProps<Namespace>
): JSX.Element {
  return <UnsafeTrans {...props} />;
}

@osdiab
Copy link

osdiab commented Nov 12, 2021

next update i'll post is to change the Paths type to support this library's _ prefixed pluralization rules; the nested plurals sound harder to express in TypeScript.

@ChristoRibeiro
Copy link

@osdiab what looks like your file?

import type { TranslationsKeys } from "src/utility/i18n/available-translations";

@osdiab
Copy link

osdiab commented Feb 26, 2022

Like this:

import type userProfile from "locales/en/user-profile.json";
import type common from "locales/en/common.json";

type Join<S1, S2> = S1 extends string
  ? S2 extends string
    ? `${S1}.${S2}`
    : never
  : never;

export type Paths<T> = {
  [K in keyof T]: T[K] extends Record<string, unknown>
    ? Join<K, Paths<T[K]>>
    : K;
}[keyof T];

export interface TranslationsKeys {
  common: Paths<typeof common>;
  "user-profile": Paths<typeof userProfile>;
}

It's a little annoying that I have to add this boilerplate to get the types of the locales - but I bet can make a script that does this kind of thing automatically.

@elementrics
Copy link

elementrics commented May 29, 2022

another way with this generator: https://www.npmjs.com/package/next-translate-localekeys. Is able to work with the basics for generating locale keys that are available and can be inserted in the useTranslation hook

@kuus
Copy link

kuus commented Jun 30, 2022

for those who are interested in a working example I setup my solution for this matter in this library https://github.com/knitkode/koine/blob/main/packages/next/types-i18n.ts
it allows to augment the namespace Koine with type NextTranslations whose keys point to the defaultLocale translation files,

PS: I am also wrapping useT and the other methods tweaking some behaviours, so there is more than type safety in that library

@quyctd
Copy link

quyctd commented Sep 25, 2022

@saschahapp nice work 🚀

@quyctd
Copy link

quyctd commented Oct 5, 2022

Is there any way to support type-safe params too?

@elementrics
Copy link

@quyctd my package does not currently support that. But if this would be an improvement, I would gladly add it.

@quyctd
Copy link

quyctd commented Oct 6, 2022

@saschahapp Yes, please consider it. Since Next.js usually come with typescript, having type-safe for both keys and params will be awesome 🚀

@mleister97
Copy link

@osdiab Any news on pluralization?

@osdiab
Copy link

osdiab commented Oct 12, 2022 via email

@luixo
Copy link

luixo commented Mar 4, 2023

Are there any plans to integrate this feature into the library?
i18next does that and it is the only feature that stops me from migrating to next-translate.

@aralroca
Copy link
Owner

aralroca commented Mar 5, 2023

Yes, we are going to priorize this. However feel free to PR 👍😊

@aralroca aralroca self-assigned this Mar 5, 2023
@aralroca aralroca added the enhancement New feature or request label Mar 5, 2023
@boredland
Copy link
Contributor

@osdiab Any news on pluralization?

You could just clean up the resulting paths using this:

type RemoveSuffix<Key extends string> = Key extends `${infer Prefix}${
  | "_zero"
  | "_one"
  | "_two"
  | "_few"
  | "_many"
  | "_other"}`
  ? Prefix
  : Key;

@X7Becka
Copy link

X7Becka commented May 10, 2023

I solved it using the following custom hook

// util-types.ts

type Join<S1, S2> = S1 extends string
  ? S2 extends string
    ? `${S1}.${S2}`
    : never
  : never;

export type Paths<T> = {
  [K in keyof T]: T[K] extends Record<string, unknown>
    ? Join<K, Paths<T[K]>>
    : K;
}[keyof T];
// useTypeSafeTranslation.ts

import useTranslation from "next-translate/useTranslation";
import { TranslationQuery } from "next-translate";
import { Paths } from "../types/util-types";

import common from "../../locales/es-es/common.json";
import home from "../../locales/es-es/home.json";
import catalog from "../../locales/es-es/catalog.json";
import auth from "../../locales/es-es/auth.json";

export type TranslationKeys = {
  common: Paths<typeof common>;
  home: Paths<typeof home>;
  catalog: Paths<typeof catalog>;
  auth: Paths<typeof auth>;
};

export const useTypeSafeTranslation = <T extends keyof TranslationKeys>(
  ns: T
) => {
  const { t, lang } = useTranslation(ns);

  return {
    t: (
      s: TranslationKeys[T],
      q?: TranslationQuery,
      o?: {
        returnObjects?: boolean;
        fallback?: string | string[];
        default?: string;
      }
    ) => t(s, q, o),
    lang,
  };
};

And then you just pass the namespace to the hook, and you have a type safe t function

import React from "react";
import { useTypeSafeTranslation } from "./useTypeSafeTranslation";

interface TestComponentProps {}

export const TestComponent: React.FC<TestComponentProps> = () => {
  const { t } = useTypeSafeTranslation("common");

  return <>{t("footer.legal.paymentMethods")}</>;
}
;
Screenshot 2021-11-11 at 13 31 04

Adapted from https://github.com/benawad/dogehouse

This little rework could help us to get dicwords from another namespaces different to default which sets in useTypeSafeTranslation.

import useTranslation from "next-translate/useTranslation";
import type { TranslationQuery } from "next-translate";
import type ProfileEn from "@/../locales/en/profile.json";
import type CommonEn from "@/../locales/en/common.json";
import type ProfileRu from "@/../locales/ru/profile.json";
import type CommonRu from "@/../locales/ru/common.json";

type Join<S1, S2> = S1 extends string
  ? S2 extends string
    ? `${S1}.${S2}`
    : never
  : never;

export type Paths<T> = {
  [K in keyof T]: T[K] extends Record<string, unknown>
    ? Join<K, Paths<T[K]>>
    : K;
}[keyof T];

type All<T> = {
  [Ns in keyof T]: `${Extract<Ns, string>}:${Extract<T[Ns], string>}`;
}[keyof T];

export interface TranslationKeys {
  common: Paths<typeof CommonRu & typeof CommonEn>;
  profile: Paths<typeof ProfileRu & typeof ProfileEn>;
}

export const useTypeSafeTranslation = <T extends keyof TranslationKeys>(
  ns: T
) => {
  const { t, lang } = useTranslation(ns);

  return {
    t: (
      s: TranslationKeys[T] | All<Omit<TranslationKeys, T>>,
      q?: TranslationQuery,
      o?: {
        returnObjects?: boolean;
        fallback?: string | string[];
        default?: string;
      }
    ) => t(s, q, o),
    lang,
  };
};

mspaint_ZFjZPiRTpy

@aralroca
Copy link
Owner

Feel free to PR improving the types

@eddyhdzg-solarx
Copy link

My current implementation works good with next.js 13 app directory.

next-translate.d.ts

import type { I18n, Translate } from "next-translate";
import type common from "~/../locales/en/common.json";
import type home from "~/../locales/en/home.json";

type Join<S1, S2> = S1 extends string
  ? S2 extends string
    ? `${S1}.${S2}`
    : never
  : never;

export type Paths<T> = {
  [K in keyof T]: T[K] extends Record<string, unknown>
    ? Join<K, Paths<T[K]>>
    : K;
}[keyof T];

export interface TranslationsKeys {
  common: Paths<typeof common>;
  home: Paths<typeof home>;
}

export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
  extends Omit<I18n, "t"> {
  t: (
    key: TranslationsKeys[Namespace],
    ...rest: Tail<Parameters<Translate>>
  ) => string;
}

declare module "next-translate/useTranslation" {
  export default function useTranslation<
    Namespace extends keyof TranslationsKeys,
  >(namespace: Namespace): TypeSafeTranslate<Namespace>;
}

@aralroca
Copy link
Owner

aralroca commented Jul 15, 2023

I modified the last example allowing plurals:

t('example', { count: 5 });  // example_other

exact matches:

t('example', { count: 99 }); // example_99

and tagged template string:

t`example` // example

next-translate.d.ts:

import type { I18n, Translate } from "next-translate";

type RemovePlural<Key extends string> = Key extends `${infer Prefix}${| "_zero"
  | "_one"
  | "_two"
  | "_few"
  | "_many"
  | "_other"
  | `_${infer Num}`}`
  ? Prefix
  : Key;

type Join<S1, S2> = S1 extends string
  ? S2 extends string
  ? `${S1}.${S2}`
  : never
  : never;

export type Paths<T> = RemovePlural<{
  [K in keyof T]: T[K] extends Record<string, unknown>
  ? Join<K, Paths<T[K]>>
  : K;
}[keyof T]>;

export interface TranslationsKeys {
  common: Paths<typeof import("./locales/en/common.json")>;
  home: Paths<typeof import("./locales/en/home.json")>;
}

export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
  extends Omit<I18n, "t"> {
  t: {
    (key: TranslationsKeys[Namespace], ...rest: Tail<Parameters<Translate>>): string;
    <T extends string>(template: TemplateStringsArray): string;
  };
}

declare module "next-translate/useTranslation" {
  export default function useTranslation<
    Namespace extends keyof TranslationsKeys,
  >(namespace: Namespace): TypeSafeTranslate<Namespace>;
}

I am thinking of adding a new configuration property to auto-generate and update this file as new namespaces are added. What do you think about this? 🤔

@osdiab
Copy link

osdiab commented Jul 16, 2023 via email

@aralroca
Copy link
Owner

aralroca commented Jul 16, 2023

I'd really like to find an elegant way to do it without needing to create next-translate.d.ts, but relying on the JSONs of the namespaces that everyone puts in is not clear to me. I don't know if pulling the types from the namespaces defined in the i18n.js file would work or not. Also it depends on where people have these namespaces, if it is the default form (inside locales/lang/namespace.json) maybe it could work. Well I'll investigate a bit more and see.

@aralroca
Copy link
Owner

for now I did a little improvement in 2.5 to simplify the next-translate.d.ts file:

Example:

import type { Paths, I18n, Translate } from 'next-translate'

export interface TranslationsKeys {
  // Example with "common" and "home" namespaces in "en" (the default language):
  common: Paths<typeof import('./locales/en/common.json')>
  home: Paths<typeof import('./locales/en/home.json')>
  // Specify here all the namespaces you have...
}

export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
  extends Omit<I18n, 't'> {
  t: {
    (
      key: TranslationsKeys[Namespace],
      ...rest: Tail<Parameters<Translate>>
    ): string
    <T extends string>(template: TemplateStringsArray): string
  }
}

declare module 'next-translate/useTranslation' {
  export default function useTranslation<
    Namespace extends keyof TranslationsKeys
  >(namespace: Namespace): TypeSafeTranslate<Namespace>
}

@sandrooco
Copy link

sandrooco commented Jul 27, 2023

@aralroca Thank you for pushing this forward!
Unfortunately I can't get it to work - is it really only this snippet that needs to be added? I also tried adding it to tsconfig.json in the include array.

Edit: I had to include the namespace in useTranslation() - default ns won't work out of the box.
Additional question: what if returnObjects is set to true? Obviously string won't work. Any idea on how we could set proper types for objects?

@aralroca
Copy link
Owner

aralroca commented Jul 27, 2023

@sandrooco it should work in the latest next-translate version. The implementation supports these 2 scenarios:

  1. namespaces in useTranslation

  1. keys in t:

goes no further. No matter what parameters you use, withreturnObjects these behaviors should work the same.

@valerioleo
Copy link

Thanks for adding this feature! very helpful!

However, I have tried to add it and I get two errors with this code:

for now I did a little improvement in 2.5 to simplify the next-translate.d.ts file:

Example:

import type { Paths, I18n, Translate } from 'next-translate'

export interface TranslationsKeys {
  // Example with "common" and "home" namespaces in "en" (the default language):
  common: Paths<typeof import('./locales/en/common.json')>
  home: Paths<typeof import('./locales/en/home.json')>
  // Specify here all the namespaces you have...
}

export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
  extends Omit<I18n, 't'> {
  t: {
    (
      key: TranslationsKeys[Namespace],
      ...rest: Tail<Parameters<Translate>>
    ): string
    <T extends string>(template: TemplateStringsArray): string
  }
}

declare module 'next-translate/useTranslation' {
  export default function useTranslation<
    Namespace extends keyof TranslationsKeys
  >(namespace: Namespace): TypeSafeTranslate<Namespace>
}
  1. That Tail is not defined. I tried to recreate it myself with something like:
    type Tail<T extends readonly any[]> = T extends readonly [any, ...infer TT] ? TT : [];
    but I'm not sure this is the exact implementation
  2. in <T extends string>(template: TemplateStringsArray): string,
    we have defined the generic T but never used. I imagine this is the intended code?
    <T extends string>(template: TemplateStringsArray): T

Thanks again for this library!

@aralroca
Copy link
Owner

aralroca commented Jul 28, 2023

@valerioleo probably depends on the TypeScript version. Feel free to PR these missing parts.

About TemplateStringsArray the only thing is ignore these cases:

t`some.key`

because for now is not possible to strong type the content of some.key in template strings.

@SutuSebastian
Copy link

@aralroca Thank you for pushing this forward! Unfortunately I can't get it to work - is it really only this snippet that needs to be added? I also tried adding it to tsconfig.json in the include array.

Edit: I had to include the namespace in useTranslation() - default ns won't work out of the box. Additional question: what if returnObjects is set to true? Obviously string won't work. Any idea on how we could set proper types for objects?

Here is a functional version that defaults to "common" namespace while also allowing it to be overridable:

import type { I18n, Paths, Translate } from "next-translate";

import EN from "@/locales/en/common.json";

interface TranslationsKeys {
  common: Paths<typeof EN>;
}

interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
  extends Omit<I18n, "t"> {
  t: {
    (
      key: TranslationsKeys[Namespace],
      ...rest: Tail<Parameters<Translate>>
    ): string;
    <T extends string>(template: TemplateStringsArray): string;
  };
}

declare module "next-translate/useTranslation" {
  export default function useTranslation<
    Namespace extends keyof TranslationsKeys,
  >(namespace: Namespace = "common"): TypeSafeTranslate<Namespace>;
}

Example

t() function:

Screenshot 2023-09-08 at 15 37 32

useTranslate() hook:

Screenshot 2023-09-08 at 15 38 01

@SutuSebastian
Copy link

Thanks for adding this feature! very helpful!

However, I have tried to add it and I get two errors with this code:

for now I did a little improvement in 2.5 to simplify the next-translate.d.ts file:

Example:

import type { Paths, I18n, Translate } from 'next-translate'

export interface TranslationsKeys {
  // Example with "common" and "home" namespaces in "en" (the default language):
  common: Paths<typeof import('./locales/en/common.json')>
  home: Paths<typeof import('./locales/en/home.json')>
  // Specify here all the namespaces you have...
}

export interface TypeSafeTranslate<Namespace extends keyof TranslationsKeys>
  extends Omit<I18n, 't'> {
  t: {
    (
      key: TranslationsKeys[Namespace],
      ...rest: Tail<Parameters<Translate>>
    ): string
    <T extends string>(template: TemplateStringsArray): string
  }
}

declare module 'next-translate/useTranslation' {
  export default function useTranslation<
    Namespace extends keyof TranslationsKeys
  >(namespace: Namespace): TypeSafeTranslate<Namespace>
}
  1. That Tail is not defined. I tried to recreate it myself with something like:
    type Tail<T extends readonly any[]> = T extends readonly [any, ...infer TT] ? TT : [];
    but I'm not sure this is the exact implementation
  2. in <T extends string>(template: TemplateStringsArray): string,
    we have defined the generic T but never used. I imagine this is the intended code?
    <T extends string>(template: TemplateStringsArray): T

Thanks again for this library!

Outdated typescript version might be the case here.

@abuu-u
Copy link

abuu-u commented Sep 8, 2023

this is what i use

  • typed namespaces in options.ns as useTranslation, if you provide other ns in options, keys you can pass to t() will also change
  • t() will return string or if options.returnObjects: true then it will return object

next-translate.d.ts

import { FieldPath, FieldPathValue, FieldValues } from './tr'

export type TranslationsKeys = {
  // I use custom loadLocaleFrom
  common: typeof import('@/app/ru.json')
  index: typeof import('@/app/[lang]/(index)/ru.json')
  contacts: typeof import('@/app/[lang]/contacts/ru.json')
}

export type Tr<
  TFieldValues extends Record<Namespace, FieldValues>,
  Namespace extends keyof TranslationsKeys,
> = Omit<I18n, 't'> & {
  t: <
    TFieldName extends FieldPath<
      TFieldValues[[OtherNamespace] extends [never]
        ? Namespace
        : OtherNamespace]
    >,
    ReturnObjects extends boolean = false,
    OtherNamespace extends keyof TranslationsKeys = never,
  >(
    key: TFieldName,
    query?: TranslationQuery | null,
    options?: {
      returnObjects?: ReturnObjects
      fallback?: string | string[]
      default?: FieldPathValue<
        TFieldValues[[OtherNamespace] extends [never]
          ? Namespace
          : OtherNamespace],
        TFieldName
      >
      ns?: OtherNamespace
    },
  ) => ReturnObjects extends true
    ? FieldPathValue<
        TFieldValues[[OtherNamespace] extends [never]
          ? Namespace
          : OtherNamespace],
        TFieldName
      >
    : string
}

declare module 'next-translate/useTranslation' {
  export default function useTranslation<
    Namespace extends keyof TranslationsKeys,
  >(namespace: Namespace): Tr<TranslationsKeys, Namespace>
}

tr.d.ts (types ripped from react-hook-form)

export type BrowserNativeObject = Date | FileList | File

export type Primitive =
  | null
  | undefined
  | string
  | number
  | boolean
  | symbol
  | bigint

export type IsEqual<T1, T2> = T1 extends T2
  ? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
    ? true
    : false
  : false

export type AnyIsEqual<T1, T2> = T1 extends T2
  ? IsEqual<T1, T2> extends true
    ? true
    : never
  : never

export type PathImpl<K extends string | number, V, TraversedTypes> = V extends
  | Primitive
  | BrowserNativeObject
  ? `${K}`
  : true extends AnyIsEqual<TraversedTypes, V>
  ? `${K}`
  : `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`

export type IsTuple<T extends ReadonlyArray<any>> = number extends T['length']
  ? false
  : true

export type ArrayKey = number

export type TupleKeys<T extends ReadonlyArray<any>> = Exclude<
  keyof T,
  keyof any[]
>

export type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<
  infer V
>
  ? IsTuple<T> extends true
    ? {
        [K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>
      }[TupleKeys<T>]
    : PathImpl<ArrayKey, V, TraversedTypes>
  : {
      [K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>
    }[keyof T]

export type FieldValues = Record<string, any>

export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>

export type Path<T> = T extends any ? PathInternal<T> : never

export type FieldPathValue<
  TFieldValues extends FieldValues,
  TFieldPath extends FieldPath<TFieldValues>,
> = PathValue<TFieldValues, TFieldPath>

export type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never

export type ArrayPathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<
  infer V
>
  ? IsTuple<T> extends true
    ? {
        [K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>
      }[TupleKeys<T>]
    : ArrayPathImpl<ArrayKey, V, TraversedTypes>
  : {
      [K in keyof T]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>
    }[keyof T]

export type ArrayPathImpl<
  K extends string | number,
  V,
  TraversedTypes,
> = V extends Primitive | BrowserNativeObject
  ? IsAny<V> extends true
    ? string
    : never
  : V extends ReadonlyArray<infer U>
  ? U extends Primitive | BrowserNativeObject
    ? IsAny<V> extends true
      ? string
      : never
    : true extends AnyIsEqual<TraversedTypes, V>
    ? never
    : `${K}` | `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
  : true extends AnyIsEqual<TraversedTypes, V>
  ? never
  : `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`

export type IsAny<T> = 0 extends 1 & T ? true : false

export type PathValue<T, P extends Path<T> | ArrayPath<T>> = T extends any
  ? P extends `${infer K}.${infer R}`
    ? K extends keyof T
      ? R extends Path<T[K]>
        ? PathValue<T[K], R>
        : never
      : K extends `${ArrayKey}`
      ? T extends ReadonlyArray<infer V>
        ? PathValue<V, R & Path<V>>
        : never
      : never
    : P extends keyof T
    ? T[P]
    : P extends `${ArrayKey}`
    ? T extends ReadonlyArray<infer V>
      ? V
      : never
    : never
  : never

@cassus
Copy link

cassus commented Oct 7, 2023

I used this to support namespaced keys

type NamespacedTranslationKeys = {
  [Namespace in keyof TranslationsKeys]: `${Namespace}:${TranslationsKeys[Namespace]}`
}[keyof TranslationsKeys]

Entire code: https://gist.github.com/cassus/cb28122d20f61d3be9c7e09d23033be9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests