diff --git a/test/typescript/custom-types/t.test.ts b/test/typescript/custom-types/t.test.ts index 9a7ce725..cf6495df 100644 --- a/test/typescript/custom-types/t.test.ts +++ b/test/typescript/custom-types/t.test.ts @@ -156,11 +156,22 @@ describe('t', () => { it('should accept a default context key as a valid `t` function key', () => { expectTypeOf(t('beverage')).toMatchTypeOf('cold water'); + + expectTypeOf(t('beverage', { context: undefined })).toMatchTypeOf('cold water'); }); it('should throw error when no `context` is provided using and the context key has no default value ', () => { // @ts-expect-error dessert has no default value, it needs a context expectTypeOf(t('dessert')).toMatchTypeOf('error'); + + // @ts-expect-error dessert has no default value, it needs a context + expectTypeOf(t('dessert', { context: undefined })).toMatchTypeOf('error'); + + // TODO: edge case which is not correctly detected currently + // expectTypeOf( + // // @ts-expect-error no default context so it must give a type error + // t('dessert', { context: undefined as 'cake' | undefined }), + // ).toMatchTypeOf(); }); it('should work with enum as a context value', () => { @@ -174,18 +185,50 @@ describe('t', () => { expectTypeOf(t('dessert', { context: ctx })).toMatchTypeOf(); }); - it('should trow error with string union with missing context value', () => { + it('should throw error with string union with missing context value', () => { enum DessertMissingValue { - COOKIE = 'cookie', CAKE = 'cake', MUFFIN = 'muffin', ANOTHER = 'another', } - const ctxMissingValue = DessertMissingValue.ANOTHER; + const getRandomDessert = (): DessertMissingValue => + Math.random() < 0.5 ? DessertMissingValue.CAKE : DessertMissingValue.ANOTHER; + + const ctxRandomValue: DessertMissingValue = getRandomDessert(); // @ts-expect-error Dessert.ANOTHER is not mapped so it must give a type error - expectTypeOf(t('dessert', { context: ctxMissingValue })).toMatchTypeOf(); + expectTypeOf(t('dessert', { context: ctxRandomValue })).toMatchTypeOf(); + + // @ts-expect-error Dessert.ANOTHER is not mapped so it must give a type error + expectTypeOf(t('dessert', { context: DessertMissingValue.ANOTHER })).toMatchTypeOf(); + + expectTypeOf( + // @ts-expect-error 'another' is not mapped so it must give a type error + t('dessert', { context: 'cake' as 'cake' | 'another' }), + ).toEqualTypeOf<'a nice cake'>(); + }); + + it('should not throw error with string union with undefined context value if it has a default context', () => { + enum BeverageValue { + BEER = 'beer', + WATER = 'water', + } + + const getRandomBeverage = (): BeverageValue | undefined => + Math.random() < 0.5 ? BeverageValue.BEER : undefined; + + const ctxRandomValue = getRandomBeverage(); + + expectTypeOf( + t('beverage', { context: ctxRandomValue }), + ).toMatchTypeOf<'a classic beverage'>(); + + expectTypeOf( + t('beverage', { context: 'beer' as 'beer' | 'water' | undefined }), + ).toEqualTypeOf<'a classic beverage'>(); + + expectTypeOf(t('beverage', { context: undefined })).toEqualTypeOf<'a classic beverage'>(); }); it('should work with string union as a context value', () => { @@ -195,12 +238,12 @@ describe('t', () => { }); // @see https://github.com/i18next/i18next/issues/2172 - // it('should trow error with string union with missing context value', () => { - // expectTypeOf( - // // @ts-expect-error - // t('dessert', { context: 'muffin' as 'muffin' | 'cake' | 'pippo' }), - // ).toMatchTypeOf(); - // }); + it('should throw error with string union with missing context value', () => { + expectTypeOf( + // @ts-expect-error + t('dessert', { context: 'muffin' as 'muffin' | 'cake' | 'pippo' }), + ).toMatchTypeOf(); + }); }); describe('context + explicit namespace', () => { diff --git a/typescript/t.d.ts b/typescript/t.d.ts index 6b02eea3..fcdc9501 100644 --- a/typescript/t.d.ts +++ b/typescript/t.d.ts @@ -205,6 +205,23 @@ export type KeyWithContext = TOpt['context'] extends ? `${Key & string}${_ContextSeparator}${TOpt['context']}` : Key; +type ContextOfKey< + Ns extends Namespace, + Key, + TOpt extends TOptions, + Keys extends $Dictionary = KeysByTOptions, + ActualNS extends Namespace = NsByTOptions, + ActualKeys = Keys[$FirstNamespace], +> = $IsResourcesDefined extends true + ? Key extends `${infer Nsp}${_NsSeparator}${infer RestKey}` + ? Nsp extends Namespace + ? ContextOfKey + : never + : ActualKeys extends `${Key extends string ? Key : never}${_ContextSeparator}${infer Context}` + ? Context + : never + : string; + export type TFunctionReturn< Ns extends Namespace, Key, @@ -264,7 +281,12 @@ export interface TFunction = TOpt & InterpolationMap, >( ...args: - | [key: Key | Key[], options?: ActualOptions] + | [ + key: Key | Key[], + options?: Omit & { + context?: ContextOfKey; + }, + ] | [key: string | string[], options: TOpt & $Dictionary & { defaultValue: string }] | [key: string | string[], defaultValue: string, options?: TOpt & $Dictionary] ): TFunctionReturnOptionalDetails;