DEV Community

Sindre Aarsaether
Sindre Aarsaether

Posted on

Global Type Augmentations with automagic IntelliSense

Automatic global type augmentations with intellisense

extend-number

If you ever wanted to add methods to a built-in type like Number or String in JavaScript, you could extend the prototype directly:

// Add helper function to number
Object.defineProperty(Number.prototype,'clamp',{

    enumerable: false,
    value: function(min, max) {
        return Math.min(Math.max(min, this), max);
    }
});

// Add setter to allow defining shortcuts for dom elements
Object.defineProperty(HTMLElement.prototype,'shortcut',{
    enumerable: false,
    set: function() {
        this._shortcut = value;
        // register key-bindings etc
    }
});
Enter fullscreen mode Exit fullscreen mode

Now, if we try to use these functions, the type-checker will flag it as errors, and intellisense will not work:

(10).clamp(5,15) // Property 'clamp' does not exist on type number

let el = document.createElement('my-component');
el.shortcut = 'ctrl+a' // Property 'shortcut' does not exist on type HTMLElement
Enter fullscreen mode Exit fullscreen mode

To enable type-checking and intellisense you will have to create a separate file where you declare the added methods:

// types/extensions.d.ts
declare global {
    interface Number {
        clamp(min:number, max: number) : number;
    }

    interface HTMLElement {
        set shortcut(value: string);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, if you make sure the .d.ts file is referenced in your project, the squiggly lines should disappear, and completions should start to work!

It is not considered good practice to extend global types like this anyways, but extending (re-opening) your own classes, and augmenting interfaces of external libraries is even more clunky, and there might be good reasons for you to do it.

In Imba, where dom elements are first-class citizens and it is rather easy to create large projects that do not depend on a bunch of external web components and libraries, extending the functionality of tags and objects is not discouraged. This is how you would do it in imba:

extend class Number
    def clamp(min\number, max\number)
        return Math.min(Math.max(min,self),max)

extend tag element
    set shortcut value
        # register key-bindings etc

let el = <div shortcut='ctrl+a'> <span> 10.clamp(5,15)

Enter fullscreen mode Exit fullscreen mode

That is all you need. Imba generates the correct typescript declarations (with type inference). Type-checking, goto definitions, auto-completions etc just works. If your project includes a mix of imba, js, and typescript it will work across all your files.

10.clamp(5,15)
let el = <div shortcut='ctrl+a'>
Enter fullscreen mode Exit fullscreen mode

This is even better than it seems. Imba also does type inference from your actual declarations, which makes things a lot less verbose. Let's allow all custom components to easily access a shared api:

import API from './api'
const api = new API

extend tag component
    get api
        api
Enter fullscreen mode Exit fullscreen mode

Now, all components Imba will have direct access to the api. Again, with intellisense.

# define a custom button
tag edit-user-button < button
    <self @click=api.editUser(data)> 'edit user'

# use it in another custom component
tag user-modal
    <self>
        <h1> "User"
        <.actions>
            <edit-user-button data=user>
            ...

# No need to pass the api down into children, or import it from every file.
Enter fullscreen mode Exit fullscreen mode

If you want to add functionality to your api without writing it all in one file, you can simply extend the class:

import API from './api'

extend class API
    def broadcast event\string, data = {}
        # do something here ...
        self
Enter fullscreen mode Exit fullscreen mode

If you would like to know more about Imba, read the latest dev.to post or go to imba.io :)

extend-element

Top comments (0)