DEV Community

tmp7
tmp7

Posted on

Interface in Vanilla JavaScript with VS Code IntelliSense

TL;DR

Here's what an interface looks like in Vanilla JavaScript. It's relying on code analysis in code editor such as VS Code IntelliSense, so might as well call it a hack:


var interface = () => null;

var InterfaceOptions = () => ({
  name: '',
}); InterfaceOptions = interface;



// usage
// =====

let opt = InterfaceOptions`` ?? {
  name: 'Bagel',
};

function createItem(options = InterfaceOptions``) {
  // ...
}

createItem(opt);
Enter fullscreen mode Exit fullscreen mode

Here's me renaming a prop in plain JS:

Image description

You create an object factory that initiates code analysis for the props (properties) and then replaces the object with a function returning null. This enables certain declaration tricks using the nullish coalescing operator (??), keeping your code neat.

Image description

Image description

It works with arrays too! See the example code in the Trivia #4 section down below.


Discovery

1) I want VS Code IntelliSense to suggest the properties of the createBox() options.

Props of function parameter


2) Using default parameter works, but I want to place it somewhere else to declutter a bit.
Declaring default parameter outside the function


3) Declaring the options outside the function creates a bug because anyone can tinker with the value.

Possible bug: accidental value change


4) So It must be an object factory. On line 5, I use backticks instead of parentheses to differentiate an "interface" from a function invocation. Actually, I should just use a unique prefix for the variable name such as InterfaceBoxOptions or something for this post, oh well!

Creating object factory for interface


5) Okay, that works, but what if I declare the options as their own variable? How am I supposed to tell IntelliSense that an object has the props of an interface?

Interface for object

InteliSense on object properties


6). As you may know, IntelliSense assumes the interface props if I first assign the interface to the object.

InteliSense assuming an object shappe


7) To my surprise, it still works even after reassigning the variable itself with a new object.

InteliSense assuming an object props after reassigning


8) But that's one line too many. I won't accept it unless it's a one-liner! But can it?

Cluttered object with interface-like declaration


9) Answer is yes, using the nullish coalescing (??) operator. This is the only way I’ve found. One problem though, to assign the new object instead of the interface, I need to somehow make the boxOptions returns null.

Using nullish coalescing to trick IntelliSense to assume object props


10) Luckily—or perhaps intentionally by design—IntelliSense keeps suggesting the initial props of the interface even after reassigning it to a function that returns null (line 5).

And just like that, I've got a working interface-like setup in vanilla JavaScript. Should probably use TypeScript from the start, but I belong to the wild west.

A working example of interface in Vanilla JavaScript


In Production

For object declaration, I write a build script to replace interfaceName ?? with empty string before passing it to Terser because the compressor doesn't judge the returned null value for the coalescing.

Before:

let opt = InterfaceOptions`` ?? {
  name: null,
}
Enter fullscreen mode Exit fullscreen mode

After:

let opt = {
  name: null,
}
Enter fullscreen mode Exit fullscreen mode

What the compressed code may look like if you don't remove the interface part:

let opt = (() => null)() ?? {
  name: null,
}
Enter fullscreen mode Exit fullscreen mode

Trivia

1. Use Var for Interfaces

For interfaces, you want to use var instead of let or const. This makes sure it got removed when you compress at top level using Terser.

var interface = () => null;

var InterfaceOptions = () => ({
  name: null,
}); InterfaceOptions = interface;
Enter fullscreen mode Exit fullscreen mode
// terser options
{
  toplevel: true,
  compress: true,
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Terser issue #572: Remove variables that are only assigned but never read.


2. Null Interface Alternative

If global interface function is not available, e.g. if you're writing library for someone else, instead of this:

var interface = () => null;
var InterfaceOptions = () => ({
  name: null,
}); InterfaceOptions = interface;
Enter fullscreen mode Exit fullscreen mode

you can do this:

var InterfaceOptions = () => ({
  name: null,
}); InterfaceOptions = () => null;
Enter fullscreen mode Exit fullscreen mode

3. Using Interface in Interface

In case you haven't figured it out, here's how you do it:

var interface = () => null;


var ITerserOptions = () => ({
  compress: ICompress``,
  mangle: IMangle``,
}); ITerserOptions = interface;

var ICompress = () => ({
  unused: false,
}); ICompress = interface;

var IMangle = () => ({
  toplevel: false,
}); ICompress = interface;
Enter fullscreen mode Exit fullscreen mode

Image description

Nice, eh?


4. Does It Work with Array?

Yeah, but you'll need a separate interface for the array for the IntelliSense to work properly. It's pretty chaotic, I'd say.

Image description

Example 1:

var interface = () => null;

// object interfaces
var IBlogPost = () => ({
    title: '',
    labels: ILabels``,
}); IBlogPost = interface;

var ILabel = () => ({
    name: '',
}); ILabel = interface;

// array interfaces
var IBlogPosts = () => ([IBlogPost``]);
IBlogPosts = interface;

var ILabels = () => ([ILabel``]);
ILabels = interface;



let posts = IBlogPosts`` ?? [
    {
        title: 'post 1',
        labels: [{
            name: 'WIP'
        }]
    },
    {
        title: 'post 2',
        labels: [
            { name: '2025' },
            { name: 'JavaScript'},
        ],
    }
]
Enter fullscreen mode Exit fullscreen mode

But it does come with perks. Now you know what to push to the array!

Image description

Example 2:

var interface = () => null;

var ITags = () => [ITag``];
var ITag = () => ({
  name: '',
})
ITags = interface;
ITag = interface;

var IBooks = () => [IBook``];
var IBook = () => ({
  title: '',
  tags: ITags``,
})
IBooks = interface;
IBook = interface;

let books = IBooks`` ?? [];

books.push({
  title: 'Dragons Lair',
  tags: [{

  }]
})

console.log(books)
Enter fullscreen mode Exit fullscreen mode

5. Does It Work Recursively?

Something like this? Nope, the code analysis will break for that specific object.

Image description

But you can always do something like this:

var interface = () => null;

var IPlayers = () => [IPlayer``];
var IPlayer = () => ({
  name: '',
  friends: [],
})
IPlayers = interface;
IPlayer = interface;

let players = IPlayers`` ?? [];
let player1 = IPlayer`` ?? {
  name: 'ExtraLemon',
}

// player 2
players.push(player1)
players.push({
  name: 'SugarCane',
  friends: [player1],
})

console.log(players)
/*
[
    {
        "name": "ExtraLemon"
    },
    {
        "name": "SugarCane",
        "friends": [
            {
                "name": "ExtraLemon"
            }
        ]
    }
]
*/
Enter fullscreen mode Exit fullscreen mode

Top comments (0)