Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add brand to TFunction type so different namespaces' TFunctions are not treated as compatible #1994

Merged
merged 1 commit into from
Jul 11, 2023

Conversation

jtbandes
Copy link
Contributor

@jtbandes jtbandes commented Jul 7, 2023

Previously, two TFunction types would be treated as compatible even if they referred to different namespaces. For example:

function bar(t: TFunction<"bar">) { }

const { t } = useTranslation("foo");
bar(t); // wrong, but no TS error :(

This PR adds a property to the TFunction interface, making it a "branded" type. Two TFunctions with different values of Ns will no longer be treated as compatible. The example above will now fail to compile.

Fixes #1941

@coveralls
Copy link

Coverage Status

coverage: 92.36%. remained the same when pulling aa21522 on foxglove:jacob/tfunction-fix into 94eb1c6 on i18next:master.

Copy link
Member

@pedrodurek pedrodurek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me! No negative impact @adrai
Thank you @jtbandes!

@adrai adrai merged commit 4c28774 into i18next:master Jul 11, 2023
5 checks passed
@adrai
Copy link
Member

adrai commented Jul 11, 2023

included in v23.2.9

@jtbandes jtbandes deleted the jacob/tfunction-fix branch July 11, 2023 21:47
@efstathiosntonas
Copy link

efstathiosntonas commented Jul 12, 2023

Hi, since this change how we should/could type t?

previous versions, worked as a charm:

import { TFunction } from "i18next";

export interface ButtonProps {
  t: TFunction;
  title: string;
}

usage:

 const { t } = useTranslation(["action_sheets", "common"]);

<Button
   t={t}
   title={t("button.close", { ns: "common" })}
 />

In 23.2.9 it throws this error:

TS2322: Type 'TFunction<[string, string], undefined>' is not assignable to type 'TFunction<"translation", undefined>'.   Type '[string, string]' is not assignable to type '"translation"'.

@esetnik
Copy link

esetnik commented Jul 12, 2023

I have a similar issue to @efstathiosntonas.

note that since "bar" comes before "foo" in the array ordering, it is considered the default namespace for the instance of t and it could be safely passed to a function as below:

function bar(t: TFunction<"bar">) { 
  // inside bar() t is constrained to only use translation keys from the "bar" namespace
}

// 
const { t } = useTranslation(["bar", "foo"]);
bar(t); // should be correct, but gives error in v23.2.10

However, if "foo" comes first in the array order then this wouldn't work because namespace prefixes would need to be used inside bar() in order to access it's values.

function bar(t: TFunction<"bar">) { 
  // inside bar() t is constrained to only use translation keys from the "bar" namespace
}

const { t } = useTranslation(["foo", "bar"]);
bar(t); // should be an error

It gets even more complicated with default namespaces:

function bar(t: TFunction<"bar">) { 
  // inside bar() t is constrained to only use translation keys from the "bar" namespace
}

const { t } = useTranslation();
bar(t); // should be an error if "bar" is not the default namespace

There are other examples to consider as well

function bar(t: TFunction<["bar"]>) { 
}

const { t } = useTranslation("bar");
bar(t);

There are other examples to consider as well

function bar(t: TFunction<["bar", "foo"]>) { 
}

const { t } = useTranslation(["foo", "bar"]);
bar(t);

We need to update the test cases to check all of these.

@adrai
Copy link
Member

adrai commented Jul 12, 2023

@jtbandes can you have a look at this please?

@esetnik
Copy link

esetnik commented Jul 12, 2023

I believe we should deprecate useTranslation(namespaces: []) signature because it is really complex to typecheck and has some unexpected behavior related to which namespace becomes the default. I'd much prefer a simplification of the API that only allows loading one namespace at a time.

const { t } = useTranslation();
const { t: tOther } = useTranslation('other');

If we did want to support loading multiple namespaces in a single useTranslation call then we should eliminate the confusion around default namespace and say that whenever multiple namespaces are loaded you must always use the namespace prefix, e.g.

  const { t } = useTranslation(['a', 'b']); // order doesn't matter
  const value = t('a:label'); // must always use `a:` there is no default

@adrai
Copy link
Member

adrai commented Jul 12, 2023

@efstathiosntonas can you try with this?

import { TFunction } from "i18next";

export interface ButtonProps {
  t: TFunction<["action_sheets", "common"]>;
  title: string;
}

@jtbandes
Copy link
Contributor Author

How about this? #1997

@adrai
Copy link
Member

adrai commented Jul 12, 2023

fyi: v23.2.11 should optimize this

@jokerosky
Copy link

Does anyone face with type error after this merged?

In case i want to pass TFunction as parameter:

function someFunction(t: TFunction<any,any>){}

....

const {t} = useTranslation([Namespaces.SomeNamespace]);
someFunction(t) ← type error happens here

Property '$TFunctionBrand' is missing in type 'TFunction<Namespaces.SomeNamespace[], undefined>' but required in type 'TFunction<any, any>'.ts(2345)

@adrai
Copy link
Member

adrai commented Jul 17, 2023

Does anyone face with type error after this merged?

In case i want to pass TFunction as parameter:


function someFunction(t: TFunction<any,any>){}



....



const {t} = useTranslation([Namespaces.SomeNamespace]);

someFunction(t) ← type error happens here

Property '$TFunctionBrand' is missing in type 'TFunction<Namespaces.SomeNamespace[], undefined>' but required in type 'TFunction<any, any>'.ts(2345)

Make sure you use the newest i18next version and remove the any, any generics

@jokerosky
Copy link

The problem has appeared after new version released ( package.json has "i18next": "^23.2.2").
Removing any (although why generics does not work when they are last resort for typings?) makes no effect.

function someFunction(t: TFunction<'namespace'[]>) {}

const { t } = useTranslation(['namespace']);

this is the error message (i've replaced project path with ****)

Argument of type 'import("****/node_modules/react-i18next/ts4.1/index").TFunction<"namespace"[], undefined>' is not assignable to parameter of type 'import("****/node_modules/i18next/index").TFunction<"namespace"[], undefined>'.
  Property '$TFunctionBrand' is missing in type 'import("****/node_modules/react-i18next/ts4.1/index").TFunction<"namespace"[], undefined>' but required in type 'import("****/node_modules/i18next/index").TFunction<"namespace"[], undefined>'.ts(2345)
index.d.ts(910, 3): '$TFunctionBrand' is declared here.
const t: TFunction<"namespace"[], undefined>

@adrai
Copy link
Member

adrai commented Jul 17, 2023

@jokerosky can you provide a reproducible github repo?
@jtbandes can you check that?

@jokerosky
Copy link

jokerosky commented Jul 17, 2023

Strange, in fresh project there is no error https://github.com/jokerosky/i18next-types-bug-sample/blob/main/src/App.tsx#L17 will check my configuration and other packages.

Thank you for the quick response and attention ❤️

@souvikjanatw
Copy link

Does anyone face this type issue after upgrading to v23.2.11?

// myFunction.spec.ts file
const tMock = jest.fn();

/* the following line is giving the error for the type of `tMock`: Argument of type 'Mock<any, any>' is not assignable to parameter of type 'TFunction<"translation", undefined>'.
  Property '$TFunctionBrand' is missing in type 'Mock<any, any>' but required in type 'TFunction<"translation", undefined>'.*/

myFunction(tMock);


// myFunction.ts file 
import { TFunction } from "i18next";

function myFunction(t: TFunction){
  // definition of the function 
}

@adrai
Copy link
Member

adrai commented Jul 24, 2023

@jtbandes can you help @souvikjanatw ?

@jtbandes
Copy link
Contributor Author

Can you use myFunction(tMock as TFunction)?

@souvikjanatw
Copy link

souvikjanatw commented Jul 25, 2023

Can you use myFunction(tMock as TFunction)?

@jtbandes It's giving the following error: Conversion of type 'Mock<any, any>' to type 'TFunction<"translation", undefined>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Property '$TFunctionBrand' is missing in type 'Mock<any, any>' but required in type 'TFunction<"translation", undefined>'.

As it says If this was intentional, convert the expression to 'unknown' first.

const tMock = jest.fn() as unknown as TFunction; // this works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

TFunction for two different namespaces are treated as compatible
8 participants