39

I want to define a global function that is available everywhere, without the need to import the module when used.

This function aims to replace the safe navigation operator (?) available in C#. For the sake of readability, I don't want to prefix the function with a module name.

Global.d.ts:

declare function s<T>(someObject: T | null | undefined, defaultValue?: T | null | undefined) : T;

Global.tsx:

///<reference path="Global.d.ts" />

export function s<T>(object: T | null | undefined, defaultValue: T | null = null = {} as T) : T {
    if (typeof object === 'undefined' || object === null)
        return defaultValue as T;
    else
        return object;
}

App.tsx (root TypeScript file):

import 'Global';

Other TSX file (method usage):

s(s(nullableVar).member).member; //Runtime error

This compiles fine, however, in the browser this throws 's is not a function'.

olivierr91
  • 1,243
  • 3
  • 13
  • 29

3 Answers3

38

You're defining the type for the compiler, but not actually attaching it to the global namespace — window in the browser, global in node. Instead of exporting it from the module, attach it. For isomorphic use, use something like...

function s() { ... }

// must cast as any to set property on window
const _global = (window /* browser */ || global /* node */) as any
_global.s = s

You can also ditch the .d.ts file and declare the type in the same file using declare global, e.g.

// we must force tsc to interpret this file as a module, resolves
// "Augmentations for the global scope can only be directly nested in external modules or ambient module declarations."
// error
export {}

declare global {
  function s<T>(someObject: T | null | undefined, defaultValue?: T | null | undefined) : T;
}

const _global = (window /* browser */ || global /* node */) as any
_global.s = function<T>(object: T | null | undefined, defaultValue: T | null = null) : T {
  if (typeof object === 'undefined' || object === null)
    return defaultValue as T;
  else
    return object;
}
caseyWebb
  • 1,997
  • 17
  • 18
  • 1
    For your first solution, I get `Property 's' does not exist on type 'Window'`. For the second solution , I get `Augmentations for the global scope can only be directly nested in external modules or ambient module declarations.`. – olivierr91 Dec 10 '17 at 15:25
  • I have also tried to wrap my function of my d.t.s file inside a `declare global { ... }`, but when using the function I get error `Cannot find name 's'` at compile time, unless I type `window.s`, which I do not want. – olivierr91 Dec 10 '17 at 15:31
  • In case anyone runs into an issue with `Cannot find name 'global'`, you need to install `@types/node` and add `"node"` to your tsconfig's `compilerOptions.types` array. If `window` is missing, add `"dom"` to `compilerOptions.lib`. – dx_over_dt Jan 16 '19 at 00:33
  • I recommend avoiding using `compilerOptions.types` entirely. With default configuration, all files in `node_modules/@types` should be included automatically. – caseyWebb Jan 17 '19 at 20:01
  • Saved my day ... this issue took me hours... ! Thanks! :) (have an angular app and node-js server sharing some code... not fun when it comes to $localize translation. With that i could fix it server-side nice! – Daniel Aug 24 '22 at 20:31
13

Thanks to @Romain Deneau. His answer worked for me. Here is my simplified one to make it look easier to get the point of his answer. (Mine assumes the scripts run on a browser. Also, I omitted signature of function s.)

Define function outside of any class.

function s() {
    console.log("invoked s()!");
}

(window as any).s = s;

Using this global function s from TypeScript class is like below;

declare var s;

export class MyClass {
    public static callFunctionS() {
        s();
    }
}
kanji
  • 729
  • 9
  • 14
10

global.ts(x) needs just a little tweak to be a valid "global module" (a module with side effects only): remove the export keyword and add some code to augment the global object. You can also provide the global declaration in the same file and remove global.d.ts.

function _s<T>(object: T | null, defaultValue: T = {} as T) : T {
    return object == null
        ? defaultValue
        : object as T;
}

// Global declaration
declare var s: typeof _s;

// Global scope augmentation
var window = window || null;
const _global = (window || global) as any;
_global.s = _s;

To use it, just import the module once, for instance in App.tsx via a global import: import './global';.

Tested with mocha, chai, ts-node:

import { expect } from 'chai';
import './global'; // To do once at app bootstrapping

describe('global s()', () => {
    it('should replace null with empty object', () => {
        const result = s(null);
        expect(result).to.eql({});
    });

    it('should replace undefined with empty object', () => {
        const result = s(undefined);
        expect(result).to.eql({});
    });

    it('should replace null with the given default value', () => {
        const defVal = { def: 'val' };
        const result = s(null, defVal);
        expect(result).to.eql({ def: 'val' });
    });

    it('should preserve defined object', () => {
        const object = { bar: 'a' };
        const result = s(object);
        expect(result).to.eql(object);
    });
});
Romain Deneau
  • 2,841
  • 12
  • 24
  • Yeah this is what I did based on CaseeyWebb's answer. Thank you – olivierr91 Dec 12 '17 at 16:10
  • @sixtstorm1: nice! Since it is the fully working answer, can you please mark it so (or at least add a "+") in order for the next reader to be redirected to it in priority instead of a answer useful but not complety working? – Romain Deneau Dec 13 '17 at 08:33