aralroca / next-translate

Next.js plugin + i18n API for Next.js 🌍 - Load page translations and use them in an easy way!
MIT License
2.66k stars 206 forks source link

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

Open opolo opened 3 years ago

opolo commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

@osdiab what looks like your file?

import type { TranslationsKeys } from "src/utility/i18n/available-translations";
osdiab commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@saschahapp nice work πŸš€

quyctd commented 2 years ago

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

elementrics commented 2 years ago

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

quyctd commented 2 years ago

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

mleister97 commented 2 years ago

@osdiab Any news on pluralization?

osdiab commented 2 years ago

No, haven’t really focused on that issue as of late.

Omar

On Tue, Oct 11 2022 at 10:57 PM, mleister97 @.***> wrote:

@osdiab https://github.com/osdiab Any news on pluralization?

β€” Reply to this email directly, view it on GitHub https://github.com/aralroca/next-translate/issues/721#issuecomment-1274730933, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAONU3W37AJHZBTIAHK2EDDWCVW27ANCNFSM5HOCJ7EA . You are receiving this because you were mentioned.Message ID: @.***>

luixo commented 1 year ago

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 commented 1 year ago

Yes, we are going to priorize this. However feel free to PR πŸ‘πŸ˜Š

boredland commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

Feel free to PR improving the types

eddyhdzg-solarx commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

Sounds like a good idea to me :)

Omar

On Sat, Jul 15 2023 at 6:59 PM, Aral Roca Gomez @.***> wrote:

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:

texample // example

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 RemovePlural = 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 = { [K in keyof T]: T[K] extends Record<string, unknown> ? Join<K, Paths<T[K]>> : K;}[keyof T]; export interface TranslationsKeys { common: RemovePlural<Paths>; home: RemovePlural<Paths>;} type TranslateKey = T extends string ? RemovePlural<Paths> : never; export interface TypeSafeTranslate extends Omit<I18n, "t"> { t: { (key: TranslationsKeys[Namespace], ...rest: Tail<Parameters>): string;

(template: TemplateStringsArray, ...keys: TranslateKey[]): string; };} declare module "next-translate/useTranslation" { export default function useTranslation< Namespace extends keyof TranslationsKeys, >(namespace: Namespace): TypeSafeTranslate;} 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? [image: πŸ€”] β€” Reply to this email directly, view it on GitHub , or unsubscribe . You are receiving this because you were mentioned.Message ID: ***@***.***>
aralroca commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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

  1. namespaces in useTranslation

  2. keys in t:

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

valerioleo commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

this is what i use

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 commented 1 year ago

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