Skip to content

Commit

Permalink
types: support nested keys in InterpolationMap (#2140)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcalexiei committed Feb 18, 2024
1 parent 2f3384b commit d7577a9
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 13 deletions.
3 changes: 3 additions & 0 deletions test/typescript/custom-types/i18next.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TestNamespaceCustomAlternate,
TestNamespaceFallback,
TestNamespaceNonPlurals,
TestNamespaceInterpolators,
} from '../test.namespace.samples';

declare module 'i18next' {
Expand All @@ -28,6 +29,8 @@ declare module 'i18next' {
nonPlurals: TestNamespaceNonPlurals;

ord: TestNamespaceOrdinal;

interpolator: TestNamespaceInterpolators;
};
}
}
46 changes: 45 additions & 1 deletion test/typescript/custom-types/t.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('t', () => {

it('should throw an error when keys are not defined', () => {
// @ts-expect-error
assertType(t('inter', { wrongOrNoValPassed: 'xx' }));
assertType(t('do_not_exists', { wrongOrNoValPassed: 'xx' }));

// @ts-expect-error
assertType(t('baz'));
Expand Down Expand Up @@ -179,4 +179,48 @@ describe('t', () => {
expectTypeOf(tOrdPlurals).not.toMatchTypeOf(tPlurals);
expectTypeOf(tPluralsOrd).toMatchTypeOf(tPlurals);
});

describe('should work with `InterpolatorMap`', () => {
const t = (() => '') as TFunction<['interpolator']>;

it('should allow anything when key is a string', () => {
expectTypeOf(t('just_a_string', { asd: '', beep: 'boop' }));
});

it('simple key', () => {
expectTypeOf(t('simple', { olim: 'yes' })).toEqualTypeOf<'This is {{olim}}'>();

// @ts-expect-error because nope isn't a valid key
expectTypeOf(t('simple', { nope: 'yes' })).toEqualTypeOf<'This is {{olim}}'>();
});

it('simple key (multiple)', () => {
type Expected = 'This has {{more}} than {{one}}';
expectTypeOf(t('simple_multiple_keys', { more: '', one: '' })).toEqualTypeOf<Expected>();

// @ts-expect-error one of the required keys is missing
expectTypeOf(t('simple_multiple_keys', { less: '', one: '' })).toEqualTypeOf<Expected>();
});

it('keypath', () => {
expectTypeOf(
t('keypath', { out: { there: 'yes' } }),
).toEqualTypeOf<'Give me one day {{out.there}}'>();

expectTypeOf(
t('keypath_with_format', { out: { there: 'yes' } }),
).toEqualTypeOf<'Give me one day {{out.there, format}}'>();
});

it('keypath deep', () => {
type Expected = '{{living.in.the}} in the sun';

expectTypeOf(t('keypath_deep', { living: { in: { the: 'yes' } } })).toEqualTypeOf<Expected>();

expectTypeOf(
// @ts-expect-error one of the required keys is missing
t('keypath_deep', { suffering: { in: { the: 'yes' } } }),
).toEqualTypeOf<Expected>();
});
});
});
14 changes: 14 additions & 0 deletions test/typescript/test.namespace.samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,17 @@ export type TestNamespaceNonPlurals = {
title: 'title';
};
};

export type TestNamespaceInterpolators = {
just_a_string: string;

simple: 'This is {{olim}}';
simple_with_format: 'This is {{olim, format}}';
simple_multiple_keys: 'This has {{more}} than {{one}}';

keypath: 'Give me one day {{out.there}}';
keypath_with_format: 'Give me one day {{out.there, format}}';
keypath_multiple: '{{some.thing}} asd {{some.else}}';

keypath_deep: '{{living.in.the}} in the sun';
};
47 changes: 46 additions & 1 deletion typescript/helpers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,49 @@ export type $OmitArrayKeys<Arr> = Arr extends readonly any[] ? Omit<Arr, keyof a

export type $PreservedValue<Value, Fallback> = [Value] extends [never] ? Fallback : Value;

export type $NormalizeIntoArray<T extends unknown | readonly unknown[]> = T extends readonly unknown[] ? T : [T];
export type $NormalizeIntoArray<T extends unknown | readonly unknown[]> =
T extends readonly unknown[] ? T : [T];

/**
* @typeParam T
* @example
* ```
* $UnionToIntersection<{foo: {bar: string} | {asd: boolean}}> = {foo: {bar: string} & {asd: boolean}}
* ```
*
* @see https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type
*/
type $UnionToIntersection<T> = (T extends unknown ? (k: T) => void : never) extends (
k: infer I,
) => void
? I
: never;

/**
* @typeParam TPath union of strings
* @typeParam TValue value of the record
* @example
* ```
* $StringKeyPathToRecord<'foo.bar' | 'asd'> = {foo: {bar: string} | {asd: boolean}}
* ```
*/
type $StringKeyPathToRecordUnion<
TPath extends string,
TValue,
> = TPath extends `${infer TKey}.${infer Rest}`
? { [Key in TKey]: $StringKeyPathToRecord<Rest, TValue> }
: { [Key in TPath]: TValue };

/**
* Used to intersect output of {@link $StringKeyPathToRecordUnion}
*
* @typeParam TPath union of strings
* @typeParam TValue value of the record
* @example
* ```
* $StringKeyPathToRecord<'foo.bar' | 'asd'> = {foo: {bar: string} & {asd: boolean}}
* ```
*/
export type $StringKeyPathToRecord<TPath extends string, TValue> = $UnionToIntersection<
$StringKeyPathToRecordUnion<TPath, TValue>
>;
15 changes: 11 additions & 4 deletions typescript/t.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { $OmitArrayKeys, $PreservedValue, $Dictionary, $SpecialObject } from './helpers.js';
import type {
$OmitArrayKeys,
$PreservedValue,
$Dictionary,
$SpecialObject,
$StringKeyPathToRecord,
} from './helpers.js';
import type {
TypeOptions,
Namespace,
Expand Down Expand Up @@ -140,9 +146,10 @@ type ParseInterpolationValues<Ret> =
| (Value extends `${infer ActualValue},${string}` ? ActualValue : Value)
| ParseInterpolationValues<Rest>
: never;
type InterpolationMap<Ret> = Record<
$PreservedValue<ParseInterpolationValues<Ret>, string>,
unknown

type InterpolationMap<Ret> = $PreservedValue<
$StringKeyPathToRecord<ParseInterpolationValues<Ret>, unknown>,
Record<string, unknown>
>;

type ParseTReturnPlural<
Expand Down
21 changes: 14 additions & 7 deletions typescript/t.v4.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { $OmitArrayKeys, $PreservedValue, $Dictionary, $SpecialObject } from './helpers.js';
import type {
$OmitArrayKeys,
$PreservedValue,
$Dictionary,
$SpecialObject,
$StringKeyPathToRecord,
} from './helpers.js';
import type {
TypeOptions,
Namespace,
Expand All @@ -7,6 +13,9 @@ import type {
TOptions,
} from './options.js';

/** @todo consider to replace {} with Record<string, never> */
/* eslint @typescript-eslint/ban-types: ['error', { types: { "{}": false } }] */

// Type Options
type _ReturnObjects = TypeOptions['returnObjects'];
type _ReturnEmptyString = TypeOptions['returnEmptyString'];
Expand All @@ -21,9 +30,6 @@ type _JSONFormat = TypeOptions['jsonFormat'];
type _InterpolationPrefix = TypeOptions['interpolationPrefix'];
type _InterpolationSuffix = TypeOptions['interpolationSuffix'];

/** @todo consider to replace {} with Record<string, never> */
/* eslint @typescript-eslint/ban-types: ['error', { types: { "{}": false } }] */

type $IsResourcesDefined = [keyof _Resources] extends [never] ? false : true;
type $ValueIfResourcesDefined<Value, Fallback> = $IsResourcesDefined extends true
? Value
Expand Down Expand Up @@ -140,9 +146,10 @@ type ParseInterpolationValues<Ret> =
| (Value extends `${infer ActualValue},${string}` ? ActualValue : Value)
| ParseInterpolationValues<Rest>
: never;
type InterpolationMap<Ret> = Record<
$PreservedValue<ParseInterpolationValues<Ret>, string>,
unknown

type InterpolationMap<Ret> = $PreservedValue<
$StringKeyPathToRecord<ParseInterpolationValues<Ret>, unknown>,
Record<string, unknown>
>;

type ParseTReturnPlural<
Expand Down

0 comments on commit d7577a9

Please sign in to comment.