Skip to content

Commit

Permalink
Support stricter typechecking for returnEmptyString and returnNull (k…
Browse files Browse the repository at this point in the history
…ey fallbacks) (#2129)
  • Loading branch information
hsource committed Jan 29, 2024
1 parent eb1207c commit 0aef924
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,11 @@ describe('i18next.t', () => {
expectTypeOf(i18next.t('bar', 'some default value')).toMatchTypeOf<unknown>();
});

it('should accept any key if default vale is provided', () => {
it('should accept any key if default value is provided', () => {
const str: string = i18next.t('unknown-ns:unknown-key', 'default value');
assertType<string>(str);
});

it('should work with null translations', () => {
expectTypeOf(i18next.t('nullKey')).toBeNull();
});

it('should work with plurals', () => {
expectTypeOf(i18next.t('plurals:foo', { count: 1 })).toBeString();
expectTypeOf(i18next.t('plurals:foo_many', { count: 10 })).toBeString();
Expand Down
18 changes: 18 additions & 0 deletions test/typescript/custom-types-edited-returns/i18next.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'i18next';

declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'custom';

// We're mostly testing for setting returnNull and returnEmptyString to their non-default values
returnNull: true;
returnEmptyString: false;

resources: {
custom: {
nullKey: null;
'empty string with {{val}}': '';
};
};
}
}
14 changes: 14 additions & 0 deletions test/typescript/custom-types-edited-returns/t.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, it, expectTypeOf } from 'vitest';
import i18next from 'i18next';

describe('main', () => {
it('should accept null translations with returnNull set to true in config', () => {
expectTypeOf(i18next.t('nullKey')).toBeNull();
});

it('should fallback for empty string translations with returnEmpty set to false in config', () => {
expectTypeOf(
i18next.t('empty string with {{val}}'),
).toEqualTypeOf<'empty string with {{val}}'>();
});
});
5 changes: 5 additions & 0 deletions test/typescript/custom-types-edited-returns/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.json",
"include": ["./**/*"],
"exclude": []
}
6 changes: 1 addition & 5 deletions test/typescript/custom-types-json-v3/i18nextT.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,11 @@ describe('i18next.t', () => {
expectTypeOf(i18next.t('bar', 'some default value')).toMatchTypeOf<unknown>();
});

it('should accept any key if default vale is provided', () => {
it('should accept any key if default value is provided', () => {
const str: string = i18next.t('unknown-ns:unknown-key', 'default value');
assertType<string>(str);
});

it('should work with null translations', () => {
expectTypeOf(i18next.t('nullKey')).toBeNull();
});

it('should work with plurals', () => {
expectTypeOf(i18next.t('plurals:foo', { count: 1 })).toBeString();
expectTypeOf(i18next.t('plurals:foo_plural', { count: 10 })).toBeString();
Expand Down
10 changes: 7 additions & 3 deletions test/typescript/custom-types/i18nextT.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,17 @@ describe('i18next.t', () => {
expectTypeOf(i18next.t('bar', 'some default value')).toMatchTypeOf<unknown>();
});

it('should accept any key if default vale is provided', () => {
it('should accept any key if default value is provided', () => {
const str: string = i18next.t('unknown-ns:unknown-key', 'default value');
assertType<string>(str);
});

it('should work with null translations', () => {
expectTypeOf(i18next.t('nullKey')).toBeNull();
it('should fallback for null translations with unset returnNull in config', () => {
expectTypeOf(i18next.t('nullKey')).toEqualTypeOf<'nullKey'>();
});

it('should accept empty string translations with returnEmpty with unset returnEmpty in config', () => {
expectTypeOf(i18next.t('empty string with {{val}}')).toEqualTypeOf<''>('');
});

it('should work with plurals', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/typescript/custom-types/t.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('t', () => {
}>();
});

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

Expand Down
1 change: 1 addition & 0 deletions test/typescript/test.namespace.samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type TestNamespaceCustom = {
qux: 'some {{val, number}}';
inter: 'some {{val}}';
nullKey: null;
'empty string with {{val}}': '';
};

export type TestNamespaceCustomAlternate = {
Expand Down
5 changes: 5 additions & 0 deletions typescript/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export type TypeOptions = $MergeBy<
*/
returnNull: false;

/**
* Allows empty string as valid translation
*/
returnEmptyString: true;

/**
* Allows objects as valid translation result
*/
Expand Down
42 changes: 26 additions & 16 deletions typescript/t.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {

// Type Options
type _ReturnObjects = TypeOptions['returnObjects'];
type _ReturnEmptyString = TypeOptions['returnEmptyString'];
type _ReturnNull = TypeOptions['returnNull'];
type _KeySeparator = TypeOptions['keySeparator'];
type _NsSeparator = TypeOptions['nsSeparator'];
Expand Down Expand Up @@ -157,23 +158,32 @@ type ParseTReturnPluralOrdinal<
string}${_PluralSeparator}ordinal${_PluralSeparator}${PluralSuffix}`,
> = Res[(KeyWithOrdinalPlural | Key) & keyof Res];

type ParseTReturn<
type ParseTReturnWithFallback<Key, Val> = Val extends ''
? _ReturnEmptyString extends true
? ''
: Key
: Val extends null
? _ReturnNull extends true
? null
: Key
: Val;

type ParseTReturn<Key, Res, TOpt extends TOptions = {}> = ParseTReturnWithFallback<
Key,
Res,
TOpt extends TOptions = {},
> = Key extends `${infer K1}${_KeySeparator}${infer RestKey}`
? ParseTReturn<RestKey, Res[K1 & keyof Res], TOpt>
: // Process plurals only if count is provided inside options
TOpt['count'] extends number
? TOpt['ordinal'] extends boolean
? ParseTReturnPluralOrdinal<Res, Key>
: ParseTReturnPlural<Res, Key>
: // otherwise access plain key without adding plural and ordinal suffixes
Res extends readonly unknown[]
? Key extends `${infer NKey extends number}`
? Res[NKey]
: never
: Res[Key & keyof Res];
Key extends `${infer K1}${_KeySeparator}${infer RestKey}`
? ParseTReturn<RestKey, Res[K1 & keyof Res], TOpt>
: // Process plurals only if count is provided inside options
TOpt['count'] extends number
? TOpt['ordinal'] extends boolean
? ParseTReturnPluralOrdinal<Res, Key>
: ParseTReturnPlural<Res, Key>
: // otherwise access plain key without adding plural and ordinal suffixes
Res extends readonly unknown[]
? Key extends `${infer NKey extends number}`
? Res[NKey]
: never
: Res[Key & keyof Res]
>;

type TReturnOptionalNull = _ReturnNull extends true ? null : never;
type TReturnOptionalObjects<TOpt extends TOptions> = _ReturnObjects extends true
Expand Down
42 changes: 26 additions & 16 deletions typescript/t.v4.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {

// Type Options
type _ReturnObjects = TypeOptions['returnObjects'];
type _ReturnEmptyString = TypeOptions['returnEmptyString'];
type _ReturnNull = TypeOptions['returnNull'];
type _KeySeparator = TypeOptions['keySeparator'];
type _NsSeparator = TypeOptions['nsSeparator'];
Expand Down Expand Up @@ -157,23 +158,32 @@ type ParseTReturnPluralOrdinal<
string}${_PluralSeparator}ordinal${_PluralSeparator}${PluralSuffix}`,
> = Res[(KeyWithOrdinalPlural | Key) & keyof Res];

type ParseTReturn<
type ParseTReturnWithFallback<Key, Val> = Val extends ''
? _ReturnEmptyString extends true
? ''
: Key
: Val extends null
? _ReturnNull extends true
? null
: Key
: Val;

type ParseTReturn<Key, Res, TOpt extends TOptions = {}> = ParseTReturnWithFallback<
Key,
Res,
TOpt extends TOptions = {},
> = Key extends `${infer K1}${_KeySeparator}${infer RestKey}`
? ParseTReturn<RestKey, Res[K1 & keyof Res], TOpt>
: // // Process plurals only if count is provided inside options
TOpt['count'] extends number
? TOpt['ordinal'] extends boolean
? ParseTReturnPluralOrdinal<Res, Key>
: ParseTReturnPlural<Res, Key>
: // otherwise access plain key without adding plural and ordinal suffixes
Res extends readonly unknown[]
? Key extends `${infer NKey extends number}`
? Res[NKey]
: never
: Res[Key & keyof Res];
Key extends `${infer K1}${_KeySeparator}${infer RestKey}`
? ParseTReturn<RestKey, Res[K1 & keyof Res], TOpt>
: // Process plurals only if count is provided inside options
TOpt['count'] extends number
? TOpt['ordinal'] extends boolean
? ParseTReturnPluralOrdinal<Res, Key>
: ParseTReturnPlural<Res, Key>
: // otherwise access plain key without adding plural and ordinal suffixes
Res extends readonly unknown[]
? Key extends `${infer NKey extends number}`
? Res[NKey]
: never
: Res[Key & keyof Res]
>;

type TReturnOptionalNull = _ReturnNull extends true ? null : never;
type TReturnOptionalObjects<TOpt extends TOptions> = _ReturnObjects extends true
Expand Down

0 comments on commit 0aef924

Please sign in to comment.