diff --git a/projects/ngx-translate/src/lib/translate.pipe.ts b/projects/ngx-translate/src/lib/translate.pipe.ts index 3be30eba..45a0a5ee 100644 --- a/projects/ngx-translate/src/lib/translate.pipe.ts +++ b/projects/ngx-translate/src/lib/translate.pipe.ts @@ -9,15 +9,15 @@ import { InterpolationParameters, Translation } from "./translate.service.interf standalone: true, pure: false, // required to update the value when the signal changes }) -export class TranslatePipe implements PipeTransform { - private translateService = inject(TranslateService); +export class TranslatePipe implements PipeTransform { + private translateService = inject>(TranslateService); private cachedSignal: Signal | null = null; private lastKey: string | null = null; private lastParams: InterpolationParameters | undefined; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - transform(query: string | undefined | null, ...args: any[]): any { + transform(query: Key | undefined | null, ...args: any[]): any { if (!query || !query.length) { return query; } diff --git a/projects/ngx-translate/src/lib/translate.service.interface.ts b/projects/ngx-translate/src/lib/translate.service.interface.ts index f47f9c70..ddda2a28 100644 --- a/projects/ngx-translate/src/lib/translate.service.interface.ts +++ b/projects/ngx-translate/src/lib/translate.service.interface.ts @@ -41,7 +41,7 @@ export interface FallbackLangChangeEvent { translations: InterpolatableTranslationObject; } -export abstract class ITranslateService { +export abstract class ITranslateService { public abstract readonly onTranslationChange: Observable; public abstract readonly onLangChange: Observable; public abstract readonly onFallbackLangChange: Observable; @@ -64,7 +64,7 @@ export abstract class ITranslateService { public abstract resetLang(lang: Language): void; public abstract instant( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Translation; @@ -83,31 +83,31 @@ export abstract class ITranslateService { * @returns A Signal that emits the translated value */ public abstract translate( - key: string | string[] | (() => string | string[]), + key: Key | Key[] | (() => Key | Key[]), params?: InterpolationParameters | (() => InterpolationParameters | undefined), lang?: Language | (() => Language | undefined), ): Signal; public abstract stream( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Observable; public abstract getStreamOnTranslationChange( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Observable; public abstract set( - key: string, + key: Key, translation: string | TranslationObject, lang?: Language, ): void; public abstract get( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Observable; @@ -125,7 +125,7 @@ export abstract class ITranslateService { ): void; public abstract getParsedResult( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): StrictTranslation | Observable; @@ -180,7 +180,7 @@ export abstract class ITranslateService { * A `null` return means the service is the terminus of its translation * fallback chain — equivalent to "is this a root?". */ - public abstract getParent(): ITranslateService | null; + public abstract getParent(): ITranslateService | null; /** * Returns the root of this service's hierarchy — the topmost service in @@ -189,5 +189,5 @@ export abstract class ITranslateService { * * A root service returns itself. */ - public abstract getRoot(): ITranslateService; + public abstract getRoot(): ITranslateService; } diff --git a/projects/ngx-translate/src/lib/translate.service.ts b/projects/ngx-translate/src/lib/translate.service.ts index 4bfd7e82..a564108e 100644 --- a/projects/ngx-translate/src/lib/translate.service.ts +++ b/projects/ngx-translate/src/lib/translate.service.ts @@ -76,7 +76,7 @@ const makeObservable = (value: T | Observable): Observable => { }; @Injectable() -export class TranslateService implements ITranslateService { +export class TranslateService implements ITranslateService { protected readonly loadingTranslations = new LoadingTranslationsRegistry(); protected lastUseLanguage: Language | null = null; @@ -86,7 +86,7 @@ export class TranslateService implements ITranslateService { protected missingTranslationHandler = inject(MissingTranslationHandler); protected store: TranslateStore = inject(TranslateStore); - protected readonly parent: TranslateService | null; + protected readonly parent: TranslateService | null; protected get isRoot(): boolean { return this.parent === null; @@ -122,9 +122,9 @@ export class TranslateService implements ITranslateService { * A root service returns itself. Equivalent to walking `getParent()` until * it returns `null`, but provided as a convenience. */ - public getRoot(): TranslateService { + public getRoot(): TranslateService { // eslint-disable-next-line @typescript-eslint/no-this-alias - let svc: TranslateService = this; + let svc: TranslateService = this; while (svc.parent) svc = svc.parent; return svc; } @@ -136,13 +136,13 @@ export class TranslateService implements ITranslateService { * A `null` return means the service is the terminus of its translation * fallback chain — equivalent to "is this a root?". */ - public getParent(): TranslateService | null { + public getParent(): TranslateService | null { return this.parent; } protected hasTranslationInChain(lang: Language): boolean { // eslint-disable-next-line @typescript-eslint/no-this-alias - for (let svc: TranslateService | null = this; svc; svc = svc.parent) { + for (let svc: TranslateService | null = this; svc; svc = svc.parent) { if (svc.store.hasTranslationFor(lang)) return true; } return false; @@ -151,7 +151,7 @@ export class TranslateService implements ITranslateService { protected chainTranslationChange$(): Observable { const streams: Observable[] = []; // eslint-disable-next-line @typescript-eslint/no-this-alias - for (let svc: TranslateService | null = this; svc; svc = svc.parent) { + for (let svc: TranslateService | null = this; svc; svc = svc.parent) { streams.push(svc.store.translationChange$); } return streams.length === 1 ? streams[0] : merge(...streams); @@ -688,7 +688,7 @@ export class TranslateService implements ITranslateService { * Returns the parsed result of the translations */ public getParsedResult( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): StrictTranslation | Observable { @@ -698,7 +698,7 @@ export class TranslateService implements ITranslateService { } protected getParsedResultForArray( - key: string[], + key: Key[], interpolateParams: InterpolationParameters | undefined, lang?: Language, ) { @@ -731,7 +731,7 @@ export class TranslateService implements ITranslateService { * @returns the translated key, or an object of translated keys */ public get( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Observable { @@ -759,7 +759,7 @@ export class TranslateService implements ITranslateService { * @returns A stream of the translated key, or an object of translated keys */ public getStreamOnTranslationChange( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Observable { @@ -792,7 +792,7 @@ export class TranslateService implements ITranslateService { * @returns A stream of the translated key, or an object of translated keys */ public stream( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Observable { @@ -827,7 +827,7 @@ export class TranslateService implements ITranslateService { * bypassing the current language and fallback chain. */ public instant( - key: string | string[], + key: Key | Key[], interpolateParams?: InterpolationParameters, lang?: Language, ): Translation { @@ -890,7 +890,7 @@ export class TranslateService implements ITranslateService { * labels = this.translate.translate(['SAVE', 'CANCEL']); */ public translate( - key: string | string[] | (() => string | string[]), + key: Key | Key[] | (() => Key | Key[]), params?: InterpolationParameters | (() => InterpolationParameters | undefined), lang?: Language | (() => Language | undefined), ): Signal { @@ -903,7 +903,7 @@ export class TranslateService implements ITranslateService { }); } - protected keyToObject(key: string | string[]) { + protected keyToObject(key: Key | Key[]) { if (Array.isArray(key)) { return key.reduce((acc: Record, currKey: string) => { acc[currKey] = currKey; @@ -917,7 +917,7 @@ export class TranslateService implements ITranslateService { * Sets the translated value of a key, after compiling it */ public set( - key: string, + key: Key, translation: string | TranslationObject, lang: Language = this.getCurrentLang()!, ): void { diff --git a/projects/ngx-translate/src/tests/translate.type-safety.spec.ts b/projects/ngx-translate/src/tests/translate.type-safety.spec.ts new file mode 100644 index 00000000..9301215f --- /dev/null +++ b/projects/ngx-translate/src/tests/translate.type-safety.spec.ts @@ -0,0 +1,61 @@ +import { TestBed } from "@angular/core/testing" +import { provideTranslateLoader, TranslatePipe, TranslateService, TranslationObject } from "../public-api" +import { provideTestableTranslateService, FakeLoader } from "./test-helpers" + +const translations: TranslationObject = {a: "A", b: {"a": "BA", "b": "BB"}, c: "C"} +type MyKeys = "a" | "b.a" | "b.b" | "c"; + +describe('Key type safety tests', () => { + it('should not have typescript errors when using TranslateService', () => { + TestBed.configureTestingModule({ + providers: [ + provideTestableTranslateService({ + loader: provideTranslateLoader(FakeLoader), + }), + ], + }); + const translate = TestBed.inject>(TranslateService); + translate.setTranslation("en", translations); + translate.use("en"); + + translate.instant('a'); + translate.stream('b.a'); + translate.get('b.b'); + + // @ts-expect-error + translate.get('c.c'); + // @ts-expect-error + translate.get('b'); + + expect(translate.instant('a')).toBe("A"); + }) + + it('should not have typescript errors when using TranslatePipe', () => { + TestBed.configureTestingModule({ + providers: [ + provideTestableTranslateService({ + loader: provideTranslateLoader(FakeLoader), + }), + { + provide: TranslatePipe, + useClass: TranslatePipe, + } + ], + }); + const translate = TestBed.inject>(TranslateService); + const translatePipe = TestBed.inject>(TranslatePipe); + translate.setTranslation("en", translations); + translate.use("en"); + + translatePipe.transform('a'); + translatePipe.transform('b.a'); + translatePipe.transform('b.b'); + + // @ts-expect-error + translatePipe.transform('c.c'); + // @ts-expect-error + translatePipe.transform('b'); + + expect(translatePipe.transform('b.a')).toBe("BA"); + }) +}) \ No newline at end of file