"Stand-alone" TypeScript declaration merging for a module from Definitely Typed

100 Views Asked by At

I cannot get TypeScript declaration merging to work on a particular module.

I provide below a contrived minimal example which shows where I am stuck. I have a single file test.ts:

import { assert } from 'chai';

function f(x: string | undefined): string {

    assert.isDefined(x);    // equivalent to `assert(x !== undefined)`
    return x.toLowerCase();

}

Compiling as follows:

npm install typescript @types/chai chai --save
npx tsc --strict --lib "ES6,DOM" test.ts

I get the error message error TS18048: 'x' is possibly 'undefined'. This is because in the TypeScript type definitions for chai we have:

isDefined<T>(value: T, message?: string): void;

I would like to "patch" this declaration with a TypeScript type assertion. The following works perfectly:

import { assert } from 'chai';

// similar to TypeScript NonNullable utility type
type NonUndefined<T> = T extends undefined ? never : T;

declare global {
    namespace Chai {
        interface Assert {
            isDefined<T>(value: T, message?: string): asserts value is NonUndefined<T>;
        }
    }
}

function f(x: string | undefined): string {

    assert.isDefined(x);
    return x.toLowerCase();

}

But, I want this declaration to be in a separate file, e.g. fix.d.ts which is automatically picked up by TypeScript so that the original code snippet works without modification.

I've tried various incantations based on TypeScript declaration merging documentation, but can't quite get it. Is this possible?

At time of writing, I npm installed TypeScript version 5.1.6, @types/chai version 4.3.5, and chai version 4.3.7.

1

There are 1 best solutions below

1
Craig  Hicks On

If you were in a hurry you could write this

function f(x: string | undefined): string {
    assert.isDefined(x);
    return x!.toLowerCase();
}

The exclamation mark tells Typescript "Treat it as known to be not undefined".

Otherwise, not sure it is a good to try to extend the Chai namespace from inside your application. I would encapsulate it -

interface MyAssert {
  isDefined<T>(x:T): asserts x is NonUndefined<T>;
};
const myassert: MyAssert = {
  isDefined<T>(x:T): asserts x is NonUndefined<T> {
    assert.isDefined(x);
  }
}
function f(x: string | undefined): string {
    myassert.isDefined(x);
    return x.toLowerCase();
}