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

Undefined as a return type for t from useTranslation is not allowed as React Element #794

Closed
danielbischoff opened this issue Mar 20, 2019 · 16 comments
Assignees

Comments

@danielbischoff
Copy link

danielbischoff commented Mar 20, 2019

Describe the bug
When you use the t function returned from useTranslation() in jsx, react/typescript will complain that undefined is not a valid JSX element type.

That is, because TFunction returns TResult and TResult can be undefined

type TFunction = <
    TResult extends string | object | Array<string | object> | undefined = string,
    TKeys extends string | TemplateStringsArray = string,
    TInterpolationMap extends object = StringMap
  >(
    key: TKeys | TKeys[],
    options?: TOptions<TInterpolationMap> | string,
  ) => TResult;

Occurs in react-i18next version
10.5.2

To Reproduce

function MyComp() {
const {t} = useTranslation();

return (<span>{t('')}</span>);
}

Expected behaviour
Using t directly in JSX should work according to docs:
https://react.i18next.com/latest/usetranslation-hook

OS (please complete the following information):

  • Device: Mac Mini
  • Browser: Chrome
  • Typescript: v 3.3.4000
@rosskevin
Copy link
Collaborator

  1. @jamuhl will you verify the result of t may be undefined? (I believe that is correct)

  2. @danielbischoff I am currently successfully using it the the manner described. Please complete the bug report by showing the error you are receiving and providing your tsconfig.

@jamuhl
Copy link
Member

jamuhl commented Mar 20, 2019

good question - in my world i don't get an undefined back from t - all i get is an empty string when calling with empty string as key...so would be nice to reproduce this...but for that i would need more informations

but guess this is more a warning of typescript (than the javascript truth of code)

@rosskevin
Copy link
Collaborator

This is a typescript issue, but ts should strictly reflect the runtime, that's why I ask. I thought an init option made undefined possible.

@jamuhl
Copy link
Member

jamuhl commented Mar 20, 2019

returnNull -> allows null as value in JSON
returnEmptyString -> allows empty string

but fallback always is key if remembering right - at least until overriding this using parseMissingHandler or postProcessor...

@danielbischoff
Copy link
Author

danielbischoff commented Mar 20, 2019

Thank you for your quick replies :).
I think if you enable --strictNullChecks (or better, enable --strict) in your config, you should see this error, because otherwise null and undefined are treated equal.

A components return value should never be undefined. It is not supported and your app will crash.
Only null is (currently) valid. You will get the following error:

Invariant Violation
ComponentName(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.

I think that t behaves correctly, but the type definitions in typescript for the return value of t are wrong. There should be no undefined, because that would be illegal.

@rosskevin
Copy link
Collaborator

@danielbischoff please provide your tsconfig and error (second request).

I already use strict and have no problem.

@danielbischoff
Copy link
Author

danielbischoff commented Mar 20, 2019

Okay so I upgraded again, and now these errors are gone. I think sth. went wrong while upgrading.
I will close this issue.
Thank you for your help!

@rosskevin
Copy link
Collaborator

Explanation here: i18next/i18next#1221 (comment)

@danielbischoff
Copy link
Author

danielbischoff commented Mar 20, 2019

Okay so I upgraded again, and now these errors are gone. I think sth. went wrong while upgrading.
I will close this issue.
Thank you for your help!

I am very confused. Because I think the type is wrong.
Because it states that the result of t might be one of the following types:

string | object | Array<string | object> | undefined = string,

But React (jsx) can only handle the type string and Array<string>. If t would return an object, that would not work.

So imo, the return type of t should only be string or null (or only string, if null is never returned)

@rosskevin
Copy link
Collaborator

@danielbischoff you are mixing up runtime vs static typechecking. See the linked ts explanation above.

Just so you are aware, we already test this so you should not see any ts error.

https://github.com/i18next/react-i18next/blob/master/test/typescript/useTranslation.test.tsx
https://github.com/i18next/react-i18next/blob/master/tsconfig.json

@danielbischoff
Copy link
Author

Okay, thank you @rosskevin !

@martinjlowm
Copy link

martinjlowm commented Nov 7, 2020

First off, sorry to raise up a question in such an old issue.

However, I've stumbled over this issue (in combination with [1]) and I suspect the nullish inference is just for convenience. Right off the bat, I don't see any obvious place where translate would ever return undefined (looking at [2]) which leads me to think that the type signature is incorrect as it doesn't reflect the actual implementation.

This could potentially result in undefined behavior and bugs, provided that the consumer expects these strict types. In fact, similarly, I don't see any way either to distinguish string outputs from object/array outputs (as is tested here: [3]). You would need runtime information to determine if it's the prior or the latter.

While I'm aware the TFunction-interface would need to change to be completely safe at runtime. I'd argue, the user should have to indicate which particular translations return objects and/or arrays. Something along the lines:

const obj = t('some:objectKey', { isObject: true }); // TFunctionResult = object
const arr = t('some:arrayKey', { isArray: true }); // TFunctionResult = Array<string>
const arrObj = t('some:arrayObjectKey', { isObject: true, isArray: true }); // TFunctionResult = Array<string | object>

As a quick fix to my actual problem I would have to augment TFunction with this overload, such that it will prioritized over the second signature (note the changed TResult):

export interface TFunction {
  <
    TResult extends NonNullable<TFunctionResult> = string,
    TKeys extends TFunctionKeys = string,
    TInterpolationMap extends object = StringMap
  >(
    key: TKeys | TKeys[],
    options?: TOptions<TInterpolationMap> | string,
  ): TResult;
  ...
}

Now, I just hope that no colleague of mine modifies the translations into an object based translation, resulting in [object Object] 😛


edit:

I guess you already support returnObjects which can provide some inference, but it breaks everything if it's configured globally. At least it's far more difficult to infer that part as there's a global client that the user doesn't control the initialization of and cannot easily pass around. He/she would have to mutate the client using the init interface and expose that mutated client as a cast version which enforces the returnObjects-based interface.

Here's an inferrable version of TFunction that's more flexible. Again, this is not correct if returnObjects is configured globally:

import { TOptions, TFunctionKeys, TFunctionResult, StringMap } from 'i18next';

declare module 'i18next' {
  type ReturnObjectsOutput = object | Array<string | object>;
  type ReturnObjectsConfig = { returnObjects: true };

  type NewTFunctionResult = Exclude<TFunctionResult, ReturnObjectsOutput>;

  export interface TFunction {
    // w/ returnObjects: true
    <
      TResult extends ReturnObjectsOutput,
      TKeys extends TFunctionKeys = string,
      TInterpolationMap extends object = StringMap
    >(
      key: TKeys | TKeys[],
      options: (TOptions<TInterpolationMap> & ReturnObjectsConfig) | string,
    ): TResult;

    // Two arguments
    <
      TResult extends NonNullable<NewTFunctionResult> = string,
      TKeys extends TFunctionKeys = string,
      TInterpolationMap extends object = StringMap
    >(
      key: TKeys | TKeys[],
      options?: TOptions<TInterpolationMap> | string,
    ): TResult;
    <
      TResult extends NewTFunctionResult = string,
      TKeys extends TFunctionKeys = string,
      TInterpolationMap extends object = StringMap
    >(
      key: TKeys | TKeys[],
      options?: TOptions<TInterpolationMap> | string,
    ): TResult;

    // Three arguments
    <
      TResult extends NonNullable<NewTFunctionResult> = string,
      TKeys extends TFunctionKeys = string,
      TInterpolationMap extends object = StringMap
    >(
      key: TKeys | TKeys[],
      defaultValue?: string,
      options?: TOptions<TInterpolationMap> | string,
    ): TResult;
    <
      TResult extends NewTFunctionResult = string,
      TKeys extends TFunctionKeys = string,
      TInterpolationMap extends object = StringMap
    >(
      key: TKeys | TKeys[],
      defaultValue?: string,
      options?: TOptions<TInterpolationMap> | string,
    ): TResult;
  }
}

@enoh-barbu
Copy link

enoh-barbu commented Nov 15, 2021

this still doesn't work for me and I don't understand, what's the fix/solution?

"i18next": "^21.4.0",
"react-i18next": "^11.13.0",

@tonghe7
Copy link

tonghe7 commented Feb 27, 2023

this still doesn't work for me and I don't understand, what's the fix/solution?

"i18next": "^21.4.0", "react-i18next": "^11.13.0",

I'm also running into this problem. The workaround (if it can be called a workaround) is t("something") as string

@rosskevin
Copy link
Collaborator

@enoh-barbu and @tonghe7 this issue is old and there are so many changes that the content is likely not relevant. If you want to know how to setup either typescript or the usages of it, just check this repo's tests and tsconfigs.

@enoh-barbu
Copy link

I don't remember what I did, but now I don't have this issue anymore

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

No branches or pull requests

6 participants