The function I created takes an optional array of objects with a name and value property. I would like to have the value property infer or pass the type of what the value is. I can get it to work properly with one object, but when there is more than one it keeps the type of the first object only. Here is the utility function:
import { Type } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
export type InputSignal = Record<string, any>
export type AdditionalProvider<K, V> = { name: K; value: Type<V> }
export type SetupTestOptions<K extends string, V> = {
setInput?: InputSignal
additionalProviders?: AdditionalProvider<K, V>[]
}
export function setupTest<T extends object, K extends string, V>(
Component: Type<T>,
options: SetupTestOptions<K, V> = {}
) {
const fixture: ComponentFixture<T> = TestBed.createComponent(Component)
const component: T = fixture.componentInstance
if (options.setInput) {
Object.keys(options.setInput).forEach(key => {
if (!options.setInput) return
fixture.componentRef.setInput(key, options.setInput[key])
})
}
const providers = <Record<K, V>>{}
if (options.additionalProviders) {
options.additionalProviders.forEach(({ name, value }) => {
providers[name] = TestBed.inject(value) <-- here is where I would like the types to be inferred.
})
}
fixture.detectChanges()
return { fixture, component, ...providers }
}
Here is an example of how it is used:
it('should route to the dashboard/home route if projectId is null', async () => {
const { fixture, component, location, ngZone, router } = setupTest(DashboardComponent, {
additionalProviders: [
{ name: 'location', value: Location },
{ name: 'router', value: Router },
{ name: 'ngZone', value: NgZone }
]
})
ngZone.run(() => {
router.initialNavigation()
component.ngOnInit()
})
fixture.whenStable().then(() => {
expect(location.path()).toBe('/dashboard/home')
})
})
I tried many variations of having type V
extend different Angular utility types and even explicitly added the types that I want in this use case. The closest I can get is to have a union of the 3 different services (Location | Router | NgZone
) but that defeats the purpose because then I have to cast all of the types when using them. I would like TypeScript to infer the correct type based on the value and pass that type to the name I am destructuring in the example.
I agree with Naren, but I also think the type inference makes tests easier to write and maintain, so here's a solution:
import { Type } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
export type InputSignal = Record<string, any>
export type AdditionalProviders = { name: string, value: Type<any> }[];
type GetType<T> = T extends { value: Type<infer V> } ? V : never;
type ProvidersReturnType<TP extends AdditionalProviders> = {
[TK in TP[number]['name']]: GetType<Extract<TP[number], { name: TK }>>;
}
export type SetupTestOptions<TProviders extends AdditionalProviders> = {
setInput?: InputSignal;
additionalProviders?: TProviders;
}
export function setupTest<T extends object, const TP extends AdditionalProviders>(
Component: Type<T>,
options: SetupTestOptions<TP> = {}
) {
const fixture: ComponentFixture<T> = TestBed.createComponent(Component)
const component: T = fixture.componentInstance
if (options.setInput) {
Object.keys(options.setInput).forEach(key => {
if (!options.setInput) return
fixture.componentRef.setInput(key, options.setInput[key])
})
}
const providers: ProvidersReturnType<TP> = {} as any;
if (options.additionalProviders) {
options.additionalProviders.forEach(({ name, value }) => {
(providers as any)[name] = TestBed.inject(value); // < --here is where I would like the types to be inferred.
});
}
fixture.detectChanges()
return { fixture, component, ...providers }
}