Search code examples
typescriptjasminematchertype-definitionjasmine-matchers

Jasmine custom matcher type definition


I am trying to add typescript definition to a jasmine matcher library.

I was able to add the matchers for the generic type T but now i want to add matchers only to DOM elements.

Digging into the jasmine type definition code I found a similar approach to ArrayLike (see here for the expect overload and here for the ArrayLikeMatchers).

So I created a similar one.

// Overload the expect
declare function expect<T extends HTMLElement>(actual: T): jasmine.DOMMatchers<T>;

declare namespace jasmine {
    // Augment the standard matchers. This WORKS!
    interface Matchers<T> {
        toBeExtensible(): boolean;
        toBeFrozen(): boolean;
        toBeSealed(): boolean;
        // ... other
    }
    // The matchers for DOM elements. This is NOT working!
    interface DOMMatchers<T> extends Matchers<T> {
        toBeChecked(): boolean;
        toBeDisabled(): boolean;
    }
}

But, is not working :(

Given the following code:

const div = document.createElement("div");
expect(div).toBeChecked();

The type checker is giving me the error:

[js] Property 'toBeChecked' does not exist on type 'Matchers'.


The only solution seems to be adding the expect overload before the generic expect (after the ArrayLike overload here) in the core jasmine library.

But... it is not doable :)

Any hint on how to properly implement a working solution?


Solution

  • The problem is that Typescript chooses overloads in declaration order and the very generic declare function expect<T>(actual: T): jasmine.Matchers<T>; will come before your overload. You might be able to find some magic ordering using /// references but I have not been able to get it to work and it would be very brittle.

    A better approach would be to add your extra functions right on Matchers<T> but constrain this to be derived from Matchers<HTMLElement>

    declare namespace jasmine {
        interface Matchers<T> {
            toBeExtensible(): boolean;
            toBeFrozen(): boolean;
            toBeSealed(): boolean;
    
            // this must be derived from Matchers<HTMLElement>
            toBeDisabled(this: Matchers<HTMLElement>): boolean;
            // or make it generic, with T extending HTMLElement if you really need the actual type for some reason 
            toBeChecked<T extends HTMLElement>(this: Matchers<HTMLElement>): boolean; 
        }
    }
    
    // usage
    const div = document.createElement("div");
    expect(div).toBeChecked(); // ok
    expect(10).toBeChecked() // error