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);
Here's me renaming a prop in plain JS:
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.
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.
2) Using default parameter works, but I want to place it somewhere else to declutter a bit.
3) Declaring the options outside the function creates a bug because anyone can tinker with the value.
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!
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?
6). As you may know, IntelliSense assumes the interface props if I first assign the interface to the object.
7) To my surprise, it still works even after reassigning the variable itself with a new object.
8) But that's one line too many. I won't accept it unless it's a one-liner! But can it?
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
.
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.
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,
}
After:
let opt = {
name: null,
}
What the compressed code may look like if you don't remove the interface part:
let opt = (() => null)() ?? {
name: null,
}
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;
// terser options
{
toplevel: true,
compress: true,
// ...
}
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;
you can do this:
var InterfaceOptions = () => ({
name: null,
}); InterfaceOptions = () => null;
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;
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.
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'},
],
}
]
But it does come with perks. Now you know what to push to the array!
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)
5. Does It Work Recursively?
Something like this? Nope, the code analysis will break for that specific object.
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"
}
]
}
]
*/
Top comments (0)