Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inferring the generic types of an Angular test utility function

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.

like image 266
Steve F. Avatar asked Nov 14 '25 10:11

Steve F.


1 Answers

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 }
}

Typescript Playground

enter image description here

like image 151
Andrei Tătar Avatar answered Nov 17 '25 09:11

Andrei Tătar



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!