Unavailable Typescript Binding
I am working in a Next.js project that is 100% TypeScript and also has linter requirements of 100% code and test coverage. However I am linking to a 3rd party library that is plain JavaScript Universal Module Definition(UMD) module and I need to call a tracking method within it. This leads to all sorts of weirdness with TypeScript because the types and methods are expected to be known as they are to be checked for type inference and validity.
Part 1
Starting point, UMB JavaScript is loaded dynamically into the webpage as a link. Think of for example loading jQuery as an external reference in a webpage.
<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
Caveat you normally would never do this it is only an example. The reason this is done is that the link here is tracking module that often changes and mirroring a stable version is always behind. There is a second part to do things this way as well when users have access to adblockers (completely legitimate and valid use I agree with) they will block the downloading of this script and then all the calls to the methods will blow up, but that case is covered after the type checking is done here.
Now for example you want to have a type with a callable method within it track
like the following.
export type Tracker = {
track: (type: string, data: object) => void
}
This will be loaded to the default window
interface of the browser so you should also create an interface specifying the type and it's expected location.
declare global {
interface Window {
ZZ: Tracker
}
}
Now it is possible to reference in your TypeScript code the track
method above in this format
import { Tracker } from '@/models/types';
...
useEffect(() => {
window?.ZZ?.track('something', { datapoint1: 1, elapsedTime: 100 });
}, [details]);
...
This handles the portion about getting a defined type from the undefined UMD JavaScript, but we also need to make a mock to keep test coverage at 100%
Define a mock function that will dispatch a custom event so it can be listened to you and attached
export const mockZZ = "mockery";
export const mockTrackFns = {
track: (a: string, b: object) => {
dispatchEvent(new CustomEvent(mockZZ, { detail: b}));
}
}
Now we can import this into the test functions and keep track of the calls like so
import { mockMT, MockTrackFns } = from './dsa';
window.ZZ = mockTrackFns;
const trackEvents = new Array<object>();
window.addEventListener(mockZZ, (e: Event) => {
const detail = (e as CustomEvent).detail;
if (detail != null) {
trackEvents.push (detail)
}
});
Part 2
Now there is the case because of this setup (third party link resource loading the untyped UMD JavaScript function that calls like the above window?.ZZ?.track
could fail because the dynamic underlying script file is blocked by an adblocker. In this case there are a variety of states the system could be in as adblocking is dynamic it could block the method track
and still allow the creation of the empty parent type object of ZZ
which means the above blows up still because track
is a function method.
For this case we still want to keep type checking and validation for all the tests and remaining code, but also verify that the method exists or a polyfill placeholder is in it's spot.
End desired code to be like this
import { getTrackerWithDefaults } from '@/utilities/track.safe';
import { Tracker } from '@/models/types';
...
useEffect(() => {
getTrackerWithDefaults().track('something', { datapoint1: 1, elapsedTime: 100 });
}, [details]);
...
When the code is available and loaded the data is sent as intended when it is not it will print the details to the console.debug
as it will make debug verification easier and would output something like this

Missing method track {datapoint1: 1, elapsedTime: 100}datapoint1: 1elapsedTime: 100[[Prototype]]: Object
Here I wish to define a fallback empty method to print the arguments back if called and second point I wish to attach it to all possible callable methods of the original Track
type as it could be called at any point.
The empty method function: const emptyMethod = (...args) => { console.debug('Missing method', ...args) };
And I can reuse the mockTrackFns
as the type to iterate over and check if all the methods have a match in the current window.ZZ
object. This all comes together in TypeScript like this
import { mockMT, MockTrackFns } = from './dsa';
import { Tracker } from '@/models/types';
export const getTrackerWithDefaults = (currentRef: object | null | undefined = window?.ZZ): Tracker => {
if (currentRef === undefined || currentRef === null) {
currentRef = {};
}
if (typeof currentRef !== 'object') {
return MockTrackFns;
}
// eslint-disable-next-line-no-console
const emptyMethod = (...args) => { console.debug('Missing method', ...args) };
Object.keys(MockTrackFns).forEach((methodName) => {
const methodExists = methodName in currentRef;
// @ts-expect-error Check dynamic type of matching method
if (methodExists === false || (typeof currentRef[methodName] !== 'function')) {
// @ts-expect-error Dynamically adding empty method
currentRef[methodName] = emptyMethod;
}
});
return currentRef as Tracker;
}
Whew finally fixed and checked and satisfying the TypeScript conditions and allowing for degraded functionality to not cause issues.