From 29f06903188cacdff7f94d341ec4cde50aaf72ee Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Thu, 7 Mar 2024 08:04:52 +0100 Subject: [PATCH] types(Trans): add typechecking on `context` prop --- TransWithoutContext.d.ts | 10 ++- test/typescript/custom-types/Trans.test.tsx | 26 +++---- .../custom-types/TransWithoutContext.test.tsx | 74 +++++++++++++++---- test/typescript/custom-types/i18next.d.ts | 12 +++ test/typescript/misc/Trans.test.tsx | 26 ++++--- tsconfig.json | 2 + 6 files changed, 107 insertions(+), 43 deletions(-) diff --git a/TransWithoutContext.d.ts b/TransWithoutContext.d.ts index bf30d2ae2..0b028fc19 100644 --- a/TransWithoutContext.d.ts +++ b/TransWithoutContext.d.ts @@ -7,14 +7,15 @@ type TransChild = React.ReactNode | Record; export type TransProps< Key extends ParseKeys, Ns extends Namespace = _DefaultNamespace, - TOpt extends TOptions = {}, KPrefix = undefined, + TContext extends string | undefined = undefined, + TOpt extends TOptions & { context?: TContext } = { context: TContext }, E = React.HTMLProps, > = E & { children?: TransChild | readonly TransChild[]; components?: readonly React.ReactElement[] | { readonly [tagName: string]: React.ReactElement }; count?: number; - context?: string; + context?: TContext; defaults?: string; i18n?: i18n; i18nKey?: Key | Key[]; @@ -29,7 +30,8 @@ export type TransProps< export function Trans< Key extends ParseKeys, Ns extends Namespace = _DefaultNamespace, - TOpt extends TOptions = {}, KPrefix = undefined, + TContext extends string | undefined = undefined, + TOpt extends TOptions & { context?: TContext } = { context: TContext }, E = React.HTMLProps, ->(props: TransProps): React.ReactElement; +>(props: TransProps): React.ReactElement; diff --git a/test/typescript/custom-types/Trans.test.tsx b/test/typescript/custom-types/Trans.test.tsx index ebb6242e2..4a0ad36fe 100644 --- a/test/typescript/custom-types/Trans.test.tsx +++ b/test/typescript/custom-types/Trans.test.tsx @@ -72,25 +72,25 @@ describe('', () => { const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); // foo - expectTypeOf< - typeof Trans<'deeper.deeeeeper', 'alternate', {}, 'foobar.deep'> - >().toBeCallableWith({ - t, - i18nKey: 'deeper.deeeeeper', - }); + expectTypeOf>().toBeCallableWith( + { + t, + i18nKey: 'deeper.deeeeeper', + }, + ); }); it('should throw error with `t` function with key prefix and wrong `i18nKey`', () => { const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); // foo - expectTypeOf< - typeof Trans<'deeper.deeeeeper', 'alternate', {}, 'foobar.deep'> - >().toBeCallableWith({ - t, - // @ts-expect-error - i18nKey: 'xxx', - }); + expectTypeOf>().toBeCallableWith( + { + t, + // @ts-expect-error + i18nKey: 'xxx', + }, + ); }); }); diff --git a/test/typescript/custom-types/TransWithoutContext.test.tsx b/test/typescript/custom-types/TransWithoutContext.test.tsx index 4c60ed062..707635d13 100644 --- a/test/typescript/custom-types/TransWithoutContext.test.tsx +++ b/test/typescript/custom-types/TransWithoutContext.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expectTypeOf } from 'vitest'; +import { describe, it, expectTypeOf, assertType } from 'vitest'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { Trans } from '../../../TransWithoutContext'; @@ -73,25 +73,25 @@ describe('', () => { const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); // foo - expectTypeOf< - typeof Trans<'deeper.deeeeeper', 'alternate', {}, 'foobar.deep'> - >().toBeCallableWith({ - t, - i18nKey: 'deeper.deeeeeper', - }); + expectTypeOf>().toBeCallableWith( + { + t, + i18nKey: 'deeper.deeeeeper', + }, + ); }); it('should throw error with `t` function with key prefix and wrong `i18nKey`', () => { const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); // foo - expectTypeOf< - typeof Trans<'deeper.deeeeeper', 'alternate', {}, 'foobar.deep'> - >().toBeCallableWith({ - t, - // @ts-expect-error - i18nKey: 'xxx', - }); + expectTypeOf>().toBeCallableWith( + { + t, + // @ts-expect-error + i18nKey: 'xxx', + }, + ); }); }); @@ -118,4 +118,50 @@ describe('', () => { }); }); }); + + describe('usage with context', () => { + it('should work with default namespace', () => { + assertType(); + + // @ts-expect-error should throw error when context is not valid + assertType(); + }); + + it('should work with `ns` prop', () => { + assertType(); + + assertType( + // @ts-expect-error should throw error when context is not valid + , + ); + }); + + it('should work with default `t` function', () => { + const { t } = useTranslation(); + + assertType(); + + // @ts-expect-error should throw error when context is not valid + assertType(); + }); + + it('should work with custom `t` function', () => { + const { t } = useTranslation('context'); + + assertType(); + + // @ts-expect-error should throw error when context is not valid + assertType(); + }); + + it('should work with `ns` prop and `count` prop', () => { + const { t } = useTranslation('plurals'); + assertType(); + }); + + it('should work with custom `t` function and `count` prop', () => { + const { t } = useTranslation('plurals'); + assertType(); + }); + }); }); diff --git a/test/typescript/custom-types/i18next.d.ts b/test/typescript/custom-types/i18next.d.ts index 309c48fdb..4420198d2 100644 --- a/test/typescript/custom-types/i18next.d.ts +++ b/test/typescript/custom-types/i18next.d.ts @@ -8,9 +8,11 @@ declare module 'i18next' { custom: { foo: 'foo'; bar: 'bar'; + some: 'some'; some_me: 'some context'; }; + alternate: { baz: 'baz'; foobar: { @@ -22,6 +24,7 @@ declare module 'i18next' { }; }; }; + plurals: { foo_zero: 'foo'; foo_one: 'foo'; @@ -29,6 +32,15 @@ declare module 'i18next' { foo_many: 'foo'; foo_other: 'foo'; }; + + context: { + dessert_cake: 'a nice cake'; + dessert_muffin_one: 'a nice muffin'; + dessert_muffin_other: '{{count}} nice muffins'; + + beverage: 'beverage'; + beverage_beer: 'beer'; + }; }; } } diff --git a/test/typescript/misc/Trans.test.tsx b/test/typescript/misc/Trans.test.tsx index cdf2714f7..ed8f4bc5b 100644 --- a/test/typescript/misc/Trans.test.tsx +++ b/test/typescript/misc/Trans.test.tsx @@ -1,4 +1,4 @@ -import { describe, expectTypeOf, it } from 'vitest'; +import { assertType, describe, expectTypeOf, it } from 'vitest'; import * as React from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -95,14 +95,14 @@ describe('', () => { ); expectTypeOf(Trans).toBeCallableWith({ parent: CustomRedComponent, children: 'Foo' }); - - Foo - ; + assertType( + + Foo + , + ); - { - /* div is the default parent */ - } - Foo; + /* div is the default parent */ + assertType(Foo); }); it('should work with `tOptions`', () => { @@ -124,9 +124,11 @@ describe('', () => { }); it('should not work with object child', () => { - - {/* @ts-expect-error */} - This {{ var: '' }} is an error since `allowObjectInHTMLChildren` is disabled - ; + assertType( + + {/* @ts-expect-error */} + This {{ var: '' }} is an error since `allowObjectInHTMLChildren` is disabled + , + ); }); }); diff --git a/tsconfig.json b/tsconfig.json index 16f2836b5..8da4bd772 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,6 @@ { + "exclude": ["example/**/*"], + "compilerOptions": { "module": "commonjs", "target": "es5",