This is a follow-up question to this one.
Given the answer on that question, let's say I have the following ambient declaration file:
declare namespace Twilio {
interface IDevice {
ready(handler: Function): void;
}
let Device: IDevice;
}
This is working nicely on my application .ts
files, Twilio.Device.ready
is fully recognized. But I have my unit tests running with Jest and AFAIK, they run on a NodeJS environment.
As an over simplified mock on my tests I have this (which are .ts
files too):
global.Twilio = {
Device: {
ready: () => {},
offline: () => {},
error: () => {},
}
};
But this Twilio
instance is not recognized. I can fix that by adding something like below to a .d.ts
file:
declare global {
namespace NodeJS {
interface Global {
Twilio: any;
}
}
}
export = {}; // This is needed otherwise `declare global` won't work
However, I can't have this piece of code on the same file as the declare namespace Twilio
I first mentioned. They need to be on separate files, otherwise global.Twilio
on my tests will be recognized but Twilio.Device
on my code won't.
So my question is, how can I get both instances of Twilio
to be recognized in the app code and in the tests code?
And as a bonus question, it would be nice if I could use the Twilio
namespace declaration, somehow, as the type of the NodeJS Twilio
global instead of any
.
EDIT:
After a nice chat with Richard Seviora, discussing all my issues, I ended up with the following twilio.d.ts
file for my project:
/**
* Namespace declaration for the Twilio.js library global variable.
*/
declare namespace Twilio {
type DeviceCallback = (device : IDevice) => void;
type ConnectionCallback = (connection : IConnection) => void;
type ErrorCallback = (error : IError) => void;
interface IConnection {
parameters: IConnectionParameters;
}
interface IConnectionParameters {
From?: string;
To?: string;
}
interface IDevice {
cancel(handler: ConnectionCallback): void;
connect(params: IConnectionParameters): IConnection;
disconnect(handler: ConnectionCallback): void;
error(handler: ErrorCallback): void;
incoming(handler: ConnectionCallback): void;
offline(handler: DeviceCallback): void;
ready(handler: DeviceCallback): void;
setup(token: string, params: ISetupParameters): void;
}
interface IError {
message?: string;
code?: number;
connection?: IConnection;
}
interface ISetupParameters {
closeProtection: boolean;
}
let Device: IDevice;
}
/**
* Augment the Node.js namespace with a global declaration for the Twilio.js library.
*/
declare namespace NodeJS {
interface Global {
Twilio: {
// Partial type intended for test execution only!
Device: Partial<Twilio.IDevice>;
};
}
}
Hopefully others find this question and Richard's answer below insightful (since the declarations documentation is kinda lacking).
Thanks again Richard for all your assistance.
This is definitely an interesting question. The problem is that the ambient file asserts that Twilio.Device
exists, and so anything like a global declaration needs to take that into account.
It turned out being rather simple to solve. There was no need to extend the declaration file or create another one. Given the declaration file you provided, all you need to do include the following in your test setup:
namespace Twilio {
Device = {
setup: function () { },
ready: function () { },
offline: function () { },
incoming: function () { },
connect: function (params): Twilio.Connection { return null },
error: function () { }
}
}
Because we declared let Twilio.Device : IDevice
, we also permitted consumers to assign Twilio.Device
at later date. This would not be possible if we declared const Twilio.Device : IDevice
.
However, we couldn't just say:
Twilio.Device = { ... }
Because doing so required the Twilio
namespace to exist, which is something that we had asserted was the case when we said declare namespace Twilio
. This Typescript would compile successfully, but the compiled JS took the existence of the ambient Twilio
variable as granted and therefore failed.
TypeScript also doesn't allow you to assign values to an existing namespace so we couldn't do:
const Twilio = {}; // Or let for that matter.
Twilio.Device = {...}
However, since TypeScript namespace declarations merge in the compiled JS, wrapping the assignment in the Twilio namespace would instantiate Twilio
(if it doesn't already exist) and then assign Device
, while all respecting the type rules stated in the ambient file.
namespace Twilio {
Device = {
// All properties need to be stubbed.
setup: function () { },
ready: function () { },
offline: function () { },
incoming: function () { },
connect: function (params): Twilio.Connection { return null },
error: function () { }
}
}
beforeEach(() => {
// Properties/mocks will need to be reset as the namespace declaration only runs once.
Twilio.Device.setup = () => {return null;};
})
test('adds stuff', () => {
expect(Twilio.Device.setup(null, null)).toBeNull()
})
This all assumes that your test files have access to the declaration file. If not you'll need to include it through tsconfig.json
, or reference it via /// <references path="wherever"/>
directive.
Edit 1
Corrected beforeEach
in the example above to return null. Otherwise test would fail.
Edit 2 - Extending NodeJS Global Declaration
I thought I should add why extending the NodeJS.Global
interface isn't necessary.
When we make an ambient declaration, it is assumed to exist as part of the global context (and any child closures). Therefore, we don't need to extend Global
with Twilio
because Twilio
is assumed to exist in the immediate context as well.
HOWEVER, this particular declaration method means that global.Twilio
does not exist. I can declare:
namespace NodeJS {
interface Global {
Twilio : {
Device : Twilio.IDevice;
};
}
}
And this will provide type inference for the Device property of the NodeJS.Global.Twilio
object, but there isn't a way to "share" namespaces in the same way we can types.
EDIT 3 - Global Extension IS Necessary After All
After conversation, we came to the conclusion that extending NodeJS.Global
is necessary because Jest does not share context between tests and the classes they're being executed in.
To allow us to modify the NodeJS.Global
interface, we append the following to the definition file:
declare namespace NodeJS {
interface Global {
Twilio: { Device: Partial<Twilio.IDevice> }
}
}
This then enables:
beforeEach(() => {
global.Twilio =
{
Device: {
setup: function () { return null },
ready: function () { },
connect: function (params): Twilio.Connection { return null },
error: function () { }
}
};
})
While this means that the NodeJS.Global.Twilio
will not be identical to the Twilio
namespace, it does permit to operate identically for the purpose of constructing tests.