I am trying to add type support for the function useTranslations
in the next-intl package.
Lets assume my ./locales/en.ts
file looks like this
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
}
}
export default dict
The useTranslations
function accepts an optional argument for namespace. It returns another function which when called has a required namespace argument. By setting the optional argument, it reduces the need to apply that part of the namespace to the required argument. Here's an example:
const t1 = useTranslations() // no optional arg
const val1 = t("one.two.three") // 3
const val2 = t("one.two.foo") // bar
const t2 = useTranslations("one.two")
const val3 = t("three") // 3
const val4 = t("foo") // bar
I have managed to get the types working for creating a string representation of the object, but I am having trouble with the first argument continuing into the second.
Here is my code so far
import dict from "./locales/en"
// `T` is the dictionary, `S` is the next string part of the object property path
// If `S` does not match dict shape, return its next expected properties
type DeepKeys<T, S extends string> = T extends object
? S extends `${infer I1}.${infer I2}`
? I1 extends keyof T
? `${I1}.${DeepKeys<T[I1], I2>}`
: keyof T & string
: S extends keyof T
? `${S}`
: keyof T & string
: ""
interface UseTranslationsReturn<D> {
<S extends string>(
namespace: DeepKeys<D, S>,
values?: Record<
string,
| string
| number
| boolean
| Date
| ((children: ReactNode) => ReactNode)
| null
| undefined
>
): string
}
interface UseTranslationsProps<D = typeof dict> {
<S extends string>(namespace?: DeepKeys<D, S>): UseTranslationsReturn<D>
}
export const useTranslations: UseTranslationsProps = (namespace) => useNextTranslations(namespace)
I am able to set the main function like,
const t = useTranslations("one.two")
However I get an error for,
const val = t("three") // Argument of type '"three"' is not assignable to parameter of type "one"
How can the type be adjusted to resolve this?
CodePudding user response:
First of all, useTranslations
should expect valid dot notation of path:
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
}
} as const
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
// type Result = "one" | "one.two" | "one.two.three" | "one.two.foo"
type Result = KeysUnion<Dict>
You can find related answers:[ here, here, here, here] and in my article
Now you know that your namespace is safe.
interface UseTranslationsProps<D = typeof dict> {
(namespace?: KeysUnion<Dict>): UseTranslationsReturn<D>
}
declare const useTranslations: UseTranslationsProps;
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
}
} as const
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
const t = useTranslations('one.two') // ok
useTranslations('hello') // expected error
useTranslation
should return curried function which in turn should expect path which is a valid property of namespace.
COnsider this example:
type RemoveDot<T extends string> = T extends `.${infer Tail}` ? Tail : T;
type ExtractString<T extends string, U extends string, Result extends string = ''> =
T extends `${infer Head}${infer Tail}` ? `${Result}${Head}` extends U ? Tail : ExtractString<Tail, U, `${Result}${Head}`> : Result
type ValidPrefix<T extends string, U extends string> = T extends `${U}${string}` ? Exclude<T, U> : never
type UseTranslationsProps<D = typeof dict> =
<
ValidKeys extends KeysUnion<D>,
Namespace extends ValidKeys
>(namespace?: Namespace) =>
(
prefix: RemoveDot<ExtractString<ValidPrefix<KeysUnion<Dict>, Namespace>, Namespace>>
) => void
declare const useTranslations: UseTranslationsProps;
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
},
bar: {
baz: 2
}
} as const
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
const t = useTranslations('one.two') // ok
useTranslations('one') // expected error
const result = t('three') // ok
t('foo') // Ok
t('one') // expected error
Now, you are allowed to use only valid prefixes to your namespace. We still need to pick apropriate property based on our namespace and prefix.
The algorithm: namespace "." prefix
Full code:
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type RemoveDot<T extends string> = T extends `.${infer Tail}` ? Tail : T;
type ExtractString<T extends string, U extends string, Result extends string = ''> =
T extends `${infer Head}${infer Tail}` ? `${Result}${Head}` extends U ? Tail : ExtractString<Tail, U, `${Result}${Head}`> : Result
type ValidPrefix<T extends string, U extends string> = T extends `${U}${string}` ? Exclude<T, U> : never
type ConcatNamespaceWithPrefix<N extends string, P extends string> = `${N}.${P}`
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
type UseTranslationsProps<D = typeof dict> =
(() => <Prefix extends KeysUnion<D>>(prefix: Prefix) => Reducer<Prefix, D>)
& (
<
ValidKeys extends KeysUnion<D>,
Namespace extends ValidKeys
>(namespace?: Namespace) =>
<Prefix extends RemoveDot<ExtractString<ValidPrefix<KeysUnion<Dict>, Namespace>, Namespace>>>(
prefix: Prefix
) => Reducer<ConcatNamespaceWithPrefix<Namespace, Prefix>, D>
)
declare const useTranslations: UseTranslationsProps;
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
},
bar: {
baz: 2
}
} as const
{
const t = useTranslations() // ok
const ok = t('one') // {two: ....}
}
{
const t = useTranslations('one.two') // ok
const ok = t('three') // 3
}
{
const t = useTranslations() // ok
const ok = t('three') // expected error
}
Please let me know if it is what you are looking for. If yes, I will write more explanation: step-by-step
I believe above code can be refactored and simplified a bit. I will provide more explanation in the evening