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

Fix context type checking #2182

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

stefan-schweiger
Copy link

@stefan-schweiger stefan-schweiger commented May 10, 2024

Fixes #2172

Checklist

  • only relevant code is changed (make a diff before you submit the PR)
  • run tests npm run test
  • tests are included
  • commit message and code follows the Developer's Certification of Origin

Checklist (for documentation change)

  • only relevant documentation part is changed (make a diff before you submit the PR)
  • motivation/reason is provided
  • commit message and code follows the Developer's Certification of Origin

@coveralls
Copy link

coveralls commented May 10, 2024

Coverage Status

coverage: 96.313%. remained the same
when pulling 55078b6 on stefan-schweiger:master
into d057e1d on i18next:master.

Copy link
Member

@marcalexiei marcalexiei left a comment

Choose a reason for hiding this comment

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

Unit test

Unit tests should be added to have this scenario tested properly.
You can add them here:

describe('context', () => {

You can update / refine the ones added via #2173.

Note

Be sure to update your branch: I have added a few more tests via #2186

Performance

I performed a benchmark on test/typescript/many-keys test scenario and tsc time went from:

npx tsc --noEmit  4.09s user 0.10s system 247% cpu 1.693 total

to

npx tsc --noEmit  5.86s user 0.23s system 206% cpu 2.942 total

Nearly ~2s up (on a MacBook Air M3).

I haven't tested in a real app.

During this tests no context value has been used, so I think that some optimisation need to be performed, especially due to the issues regarding out of memory and long type-checking time.

typescript/t.d.ts Outdated Show resolved Hide resolved
@stefan-schweiger
Copy link
Author

Unit test

Unit tests should be added to have this scenario tested properly. You can add them here:

describe('context', () => {

You can update / refine the ones added via #2173.

Note

Be sure to update your branch: I have added a few more tests via #2186

Added/extended tests, still all green.

Performance

I performed a benchmark on test/typescript/many-keys test scenario and tsc time went from:

npx tsc --noEmit  4.09s user 0.10s system 247% cpu 1.693 total

to

npx tsc --noEmit  5.86s user 0.23s system 206% cpu 2.942 total

Nearly ~2s up (on a MacBook Air M3).

I haven't tested in a real app.

During this tests no context value has been used, so I think that some optimisation need to be performed, especially due to the issues regarding out of memory and long type-checking time.

I have no real clue about typescript performance optimisations. If you have any concrete suggestions please go ahead.

@marcalexiei
Copy link
Member

In my real application time goes up from 129.95s to 147.57s.

The performance is affected whether you provide a context to a t function or not.
In my opinion this should not be merged as it is.
Up to @adrai to decide.


I have no real clue about typescript performance optimisations

You can find some information about ts performance here:
https://github.com/microsoft/TypeScript/wiki/Performance#performance-tracing

and you can test them in i18next here:
https://github.com/i18next/i18next/blob/master/test/typescript/many-keys/PERF_README.md


If you have any concrete suggestions please go ahead.

I don't have time to perform an in depth review at this time.
The only suggestion I have, so far, is to try to find a way to compute ContextOfKey only when context key is provided.
This is something similar to what has been done in #2095

@adrai
Copy link
Member

adrai commented May 13, 2024

The performance implication is to heavy... so please @stefan-schweiger try to find a way to way to compute ContextOfKey only when context key is provided, like suggested above.

@stefan-schweiger
Copy link
Author

@marcalexiei how do you arrive at those performance numbers? running time npx tsc --noEmit in the test/typescript/many-keys takes 5.9 seconds (M1 Pro) for me no matter if my code is there or not. Is there some cache or something I'm not taking into account?

@stefan-schweiger
Copy link
Author

I've now even cloned a completely fresh master branch from your repo and I still get the same performance time. Are you sure this is an issue caused by my PR?

@marcalexiei
Copy link
Member

marcalexiei commented May 13, 2024

upstream remote is i18next
context-fix is your repository

Screenshot 2024-05-13 at 10 27 06

@stefan-schweiger
Copy link
Author

stefan-schweiger commented May 13, 2024

Well that's just not what's happening for me. Where do we go from here?

image

@stefan-schweiger
Copy link
Author

@adrai can you maybe confirm either result?

@marcalexiei
Copy link
Member

I'm trying on another machine

@marcalexiei
Copy link
Member

On M2 Max there is no performance issue...

@marcalexiei
Copy link
Member

After you added improve readability of typing commit the performance issue is no longer present

@marcalexiei
Copy link
Member

Screenshot 2024-05-13 at 10 59 47

@stefan-schweiger
Copy link
Author

I'm not sure I see how moving the code around a bit would make such a difference, but I guess I'll take it. So this can be merged then?

@marcalexiei
Copy link
Member

I'm not sure I see how moving the code around a bit would make such a difference

Actually it does: you moved the context validation from the ActualOptions type parameter to the function parameters. This likely makes ts compute ContextOfKey only when necessary.

So this can be merged then?

Since the implementation is changed I would like to perform additional checks, including a real application.
I'll review the new implementation later since I no longer have time today.

@marcalexiei marcalexiei self-requested a review May 13, 2024 09:26
Comment on lines 205 to 209
// 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: 'cake' as 'cake' | undefined }),
// ).toEqualTypeOf<never>();
Copy link
Author

Choose a reason for hiding this comment

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

@marcalexiei while using this in my actual app I found this edge case which is uncovered. Wasn't able to figure out how to correctly handle this yet, maybe you have an idea. Overall I think it's not a blocker for the PR as it's still an improvement over the current functionality, but if possible to find a solution for this as well it would be great.

Copy link
Author

Choose a reason for hiding this comment

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

also can in theory be covered by writing code like this value ? t('dessert', { context: value }) : t('dessert'). Which is not ideal but helps.

Copy link
Member

@marcalexiei marcalexiei left a comment

Choose a reason for hiding this comment

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

I discovered a few regressions:

  1. usage with a t function using multiple namespaces
  2. usage with a t function with an explicit namespace inside the key

I created a PR on master with the additional test cases: #2188

@stefan-schweiger
Copy link
Author

To be honest, this is getting above my knowledge of the type system, I have tried to tinker around a bit but with no real solution in sight. I'm having a hard time to figure out how the whole NS/Prefix detection is meant to work, so if you can provide any input it would be appreciated.

@stefan-schweiger
Copy link
Author

Ok, I'm not sure I understand the solution, but somehow I got the tests to be green again, please take a look if whatever I did make sense

@marcalexiei marcalexiei self-requested a review May 14, 2024 11:44
Copy link
Member

@marcalexiei marcalexiei left a comment

Choose a reason for hiding this comment

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

@stefan-schweiger request a review from me when you are done with your changes.
I don't want to risk performing tests on outdated code or code that you are working on.

@stefan-schweiger
Copy link
Author

stefan-schweiger commented May 14, 2024

@stefan-schweiger request a review from me when you are done with your changes. I don't want to risk performing tests on outdated code or code that you are working on.

I'm still trying to figure out a solution for this t('dessert', { context: undefined as 'cake' | undefined }) but so far no luck, for now I guess go ahead with the review, if I find a solution for this I will do it in a separate PR

Copy link
Member

@marcalexiei marcalexiei left a comment

Choose a reason for hiding this comment

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

Performance

Real app

Current version (23.11.4) With the fix
~120.61s ~129.01s

I think that the performance increase is within an acceptable limit,
probably it can be improved further later.

Many keys test scenario (no context involved)

Current version (23.11.4) With the fix
~4.3s ~4.3s

Breaking changes

While testing the patch I had to make some changes in the code.

Note

All the following scenarios were working with version 23.11.4

Empty string and falsy values

Those were accepted by the previous version when the context has a default value,
however now I have to set undefined to remove the type error.

// touch is a boolean
- touch && 'touch',
+ touch  ? touch : undefined,
- typeof activeStepIndex === 'number' ? 'step' : ''
+ typeof activeStepIndex === 'number' ? 'step' : undefined

Default context value

When you access a context with a variable and that context has a default value but the i18n resource hasn't mapped all the value a type error is present.
This makes the context default value not useful in this type of scenarios.

Given the following resource structure:

"theme_setting": {
  "value": "system",
  "value|dark": "dark",
  "value|light": "light"
},

an this code

const currentTheme = 'system' as 'dark' | 'light' | 'system';
t('generic:theme_setting.value', { context: currentTheme })

To remove the error I had to remove the default context value and add the missing value (system):

-   "value": "system",
+   "value|system": "system",

Conclusion

These breaking changes make context type-check much more strict than the previous version.

I don't have a solution to make the new strict validation compatible with the old ts API.

Making a release at this point would be a major bump and due to the breaking changes above it might require a migration guide (from a TS validation standpoint).

@stefan-schweiger
Copy link
Author

Empty string and falsy values

Those were accepted by the previous version when the context has a default value, however now I have to set undefined to remove the type error.

// touch is a boolean
- touch && 'touch',
+ touch  ? touch : undefined,
- typeof activeStepIndex === 'number' ? 'step' : ''
+ typeof activeStepIndex === 'number' ? 'step' : undefined

I think that should be solvable by simply adding false | '' | 0 to the type definition. Tests are still green if I do that, but I might need to add additional tests.

Default context value

When you access a context with a variable and that context has a default value but the i18n resource hasn't mapped all the value a type error is present. This makes the context default value not useful in this type of scenarios.

Given the following resource structure:

"theme_setting": {
  "value": "system",
  "value|dark": "dark",
  "value|light": "light"
},

an this code

const currentTheme = 'system' as 'dark' | 'light' | 'system';
t('generic:theme_setting.value', { context: currentTheme })

To remove the error I had to remove the default context value and add the missing value (system):

-   "value": "system",
+   "value|system": "system",

This might be a bit harder, but let me try to give it a go as well.

@stefan-schweiger
Copy link
Author

Btw is was recommended to me by members of your organisation before that contexts are for known values and should not use defaults. So for the second one you could maybe even argue it's a feature... but let me know if I still should try to fix the typing for it.

i18next/react-i18next#1730 (comment)

@marcalexiei
Copy link
Member

I personally don't have any problem changing the code to provide only valid context value,
so support falsy values commit can be reverted also since your last comment:

Btw is was recommended to me by members of your organisation before that contexts are for known values and should not use defaults. So for the second one you could maybe even argue it's a feature... but let me know if I still should try to fix the typing for it.

Probably someone will occur in these errors while upgrading. Up to @adrai to decide.
FWIW this can be merged for me.

@stefan-schweiger
Copy link
Author

To be honest, I never 100% agreed with the assessment that context is only for known values and you even mention the fallback case in your documentation. In the end whatever you decide is fine by me, but it should be reflected in the documentation as well.

@adrai
Copy link
Member

adrai commented May 15, 2024

Btw is was recommended to me by members of your organisation before that contexts are for known values and should not use defaults. So for the second one you could maybe even argue it's a feature... but let me know if I still should try to fix the typing for it.

i18next/react-i18next#1730 (comment)

I think that was a misunderstanding...
Jan said that "context is more for a known set of option", but that does not mean it can't be used without a known value... at least at runtime this for sure is not a problem.
So this is/was just a TypeScript "problem".

So @marcalexiei and @stefan-schweiger should we merge this PR and release a new version? (patch/minor/major)?

@stefan-schweiger
Copy link
Author

@adrai in this case there is still the gap mentioned by @marcalexiei

Default context value

When you access a context with a variable and that context has a default value but the i18n resource hasn't mapped all the value a type error is present. This makes the context default value not useful in this type of scenarios.

Given the following resource structure:

"theme_setting": {
  "value": "system",
  "value|dark": "dark",
  "value|light": "light"
},

an this code

const currentTheme = 'system' as 'dark' | 'light' | 'system';
t('generic:theme_setting.value', { context: currentTheme })

To remove the error I had to remove the default context value and add the missing value (system):

-   "value": "system",
+   "value|system": "system",

For t function parameters this is relatively easy to solve, but I'm currently pulling my hair out about the TFunctionReturn type because ActualKey is always defined to include the context which might not exist in the resources. So any help from your team would be appreciated.

@marcalexiei
Copy link
Member

So @marcalexiei and @stefan-schweiger should we merge this PR and release a new version? (patch/minor/major)?

My main concern that is holding me back from approval is that now the types make context type-checking much more stricter than the JS API.

I understand the need to have a stricter validation on all possible values of a context, but is really necessary if I had a default value for the context?
The more I think about this, the more I think that strict validation should be performed only when there isn't a default value for the context.

Like I told earlier I'm too busy to perform an in depth review at this time.
I can find some time to review PRs if required, like I did here, but not much more.
At least not for the next weeks.

@stefan-schweiger
Copy link
Author

I also won't be available for the next two weeks starting tomorrow, so let's see were we stand once I'm back..

@marcalexiei
Copy link
Member

Added a new test for #2182 (review) ("Default context value" section) via #2192

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.

Not existing context not detected as type error if covered by string union
4 participants