feat(translate): typed keys for TranslateService and TranslatePipe#1620
feat(translate): typed keys for TranslateService and TranslatePipe#1620GuyHaviv37 wants to merge 2 commits into
Conversation
|
Thanks for this, @GuyHaviv37 — really appreciate the work, and the restraint in keeping it focused. Typed keys are something people have asked for, and your PR is clean, fully backward-compatible (the Key = string default is exactly right), and the @ts-expect-error spec is a nice way to lock the contract. I verified it compiles and the type checks genuinely fire, so the mechanism is solid. Before we merge I want to talk through the shape, because while digging into it I hit a wall that I think changes the direction. The per-instance generic can't reach templates. Angular template syntax has no way to pass a type argument to a pipe or directive, so {{ 'key' | translate }} always resolves to TranslatePipe regardless of the generic, and the [translate] directive stays untyped. Since most keys live in templates, the generic ends up protecting the surface that needed it least. On the imperative side it works, but only if you write inject<TranslateService>(...) at every injection site. There's one mechanism that does reach templates: a globally-augmentable key type (the i18next CustomTypeOptions pattern). Instead of a per-instance generic, the library exports an empty interface that the consumer augments once via declaration merging. Because the resulting key type is ambient, Angular's strict-template checker enforces it on the pipe and directive too. And we can have the service generic default to it, so plain inject(TranslateService) gets typed keys on every method with no per-callsite ceremony: That covers get / instant / stream / translate / the pipe / the directive — all at once, automatically. And the keys can come from the JSON rather than a hand-maintained union. A small recursive type derives the dotted-leaf paths from a statically-imported translation file: I prototyped both pieces and confirmed they compile and enforce correctly. The combination is a strict superset of what this PR does, and a power user can still pass an explicit generic to override the key-space for a specific subtree. The tradeoffs to weigh: the registry is global (one key-space per app, which is fine for the vast majority), and DeepKeys over a very large dictionary can stress the TypeScript compiler — both known from the i18next ecosystem, so we'd want a stress test before committing. I've added this to our backlog as the direction to pursue. Would you be interested in taking a run at the registry + DeepKeys version? Happy to sketch the API surface and the strict-template test with you. Either way, thank you — this PR is what surfaced the right design. |
Summary
Keygeneric toTranslateService,ITranslateService, andTranslatePipe(defaults tostringfor full backward compatibility)get,stream,instant,getStreamOnTranslationChange,getParsedResult,set,translate, andTranslatePipe.transformasKey | Key[]instead ofstring | string[]translate.type-safety.spec.tswith@ts-expect-errorassertions to lock in the type contractMotivation
Translation keys are currently plain strings, so typos and invalid nested paths (e.g.
'b'instead of'b.a') are only caught at runtime. By parameterizing the service and pipe with a key type, apps can define their known keys once and have TypeScript enforce them across the main lookup APIs.Usage
Existing code that does not pass a generic continues to work unchanged —
TranslateServiceandTranslatePipedefault toKey = string.Author's note
I tried keeping the PR as simple as possible and not to over-complicate things while having some validation and assurance with the simple test case.
If needed and per guidance of the maintainers, we can add more layers of testing and/or type-safety overhaul.
Thanks!