0

I'm trying to implement/expose functionality without using classes in Typescript.

const cacheMap = new Map<string, string>();
export function add(key, value) {
    this.cacheMap.set(key, value)
}
export function remove(key) {
    this.cacheMap.set(key)
}

Question 1: What is this pattern called? Can I export functions without class and without direct reference to cacheMap?

Question 2: Can I refer cache map using this keyword in methods like - this.cacheMap.set(key, value)?

6
  • 2
    this.cacheMap will likely be undefined. You probably don't need to use this at all here, just reference the Map directly cacheMap.set Commented May 24, 2021 at 9:51
  • You won't succeed. The main goal of Typescript is to provide TypeSafety and a more structured way of programming. Classes are a core concept. If you really want to circumvent it, you can directly stick with plain JS. Commented May 24, 2021 at 9:52
  • 2
    @Lynx242 . . . JavaScript itself is valid TypeScript and with loose configuration settings it should compile just fine. That means, that in TypeScript you can effectively use everything that is allowed/supported in JavaScript. Commented May 24, 2021 at 9:59
  • 1
    Typescript does not need classes per definition. Typescript is invented to give better type checking. You can use any style of programming you want. In this case, the OP still keeps a state in a file, so that is still OOP if you ask me. But you can write it like functions. Commented May 24, 2021 at 10:01
  • 3
    @Lynx242 . . . You are correct that TypeScript adds typing to JavaScript, making it "safer", since the compiler can catch typing errors prematurely. But it is not limited to only classes. Those additional TypeScript typing features can be applied to everything that is supported in JavaScript. Commented May 24, 2021 at 10:07

3 Answers 3

4

Long story short:

  1. Yes, you can (but without using this)
  2. No, this does not mean what you expect it to be

Exporting and closures

In JavaScript (and thus Typescript, as it compiles down to JS) you can export constants and functions directly. From this point of view, there's no difference between a class, a function, or a constant. This becomes more clear if you write a function like this:

export const add = (key, value) => { /*...*/ };

The exported functions will be able to access the cacheMap, and it will be the same object for every time you call this functions. The module itself is evaluated only once, no matter how much times do you import it, so the new Map() part will be executed only once, and only one Map will be created. The object is accessible to these functions because they capture it inside their closure, which is an important language concept for JavaScript. Here is a simple example that illustrates how closure works (but I highly recommend reading a couple of articles on it to understand it completely):

function makeCounter() {
  let i = 0; // this variable is inside a closure
  return function incrementer() {
    // a function declared within a body of another function
    // has access to all variables (and functions) declared
    // within that body via closure
    ++i;
    return i;
  };
}

const counter = makeCounter(); // function call creates a closure with variable `i`
// and all calls below use the same variable from this closure
console.log(counter()); // outputs '1'
console.log(counter()); // outputs '2'
console.log(counter()); // outputs '3'

this

this keyword is quite a misleading concept in JS (or at least unless you are used to it). It is an implicit argument passed to the function when you call it like this:

function f() { console.log(this); }

const obj: any = {x: 1}; // just an object
obj.f = f;
obj.f(); // outputs '{x: 1, f: function}'
// an object before `.f()` is passed to f as 'this'

So using this outside of class methods and some other specific cases is possible, but makes a very little sense. There are some 'reflectish' ways to pass a desired value as this argument into a function, like f.apply({/* object that will be accessible as 'this' inside function body */}), but I highly recommend just using regular arguments for your functions. The this keyword in JS is just another topic that worth reading an article or tutorial on it.

Sign up to request clarification or add additional context in comments.

2 Comments

How would we test this, can we mock module map using jest.mock?
I'm not a jest expert, but i suppose that this map cannot be mocked since it's not visible from anywhere outside the module. The map is module's private implementation detail. But you can check some invariants that add, remove (and probably some other functions you export from this module) should obey. I cannot come up with a test for add/remove pair, but if you have, for example, get function, you can test that after you add a cache entry, it will be accessible via get, and will stop being accessible after you remove it.
4

The idea behind your code is valid and should work. I doubt if you could call this a pattern; it's just JavaScript modules functionality.

Your code does contain some bugs, however:

  • You should type your parameters.
  • You should not need the this keyword in your functions here, since they are not class methods.
  • You should probably use cacheMap.delete(key) instead of cacheMap.set(key) in the remove function.

This should work just fine:

const cacheMap = new Map<string, string>();
export function add(key: string, value: string) {
  cacheMap.set(key, value);
}
export function remove(key: string) {
  cacheMap.delete(key);
}

4 Comments

Can you explain more - "You should type your parameters."?
@Prakhar . . . What I meant with that is that you should specify a type for the key and value parameters of your functions. A string in this case, since that is also the type of the keys and values in your Map object.
How would we create copy for test cases? Do we mock cacheMap for test cases?
@Prakhar . . . This module (just like the sample code in your question) uses global functionality with side effects, so I guess it will be relatively difficult to replace it with a mock object for test cases. If you want to create multiple copies of this functionality (either for testing purposes or for supporting multiple caches in your application), I would personally consider putting this logic in a class. But you explicitly specified in your question that you don't want to use classes, so I probably cannot help you with that here.
1

You can use a separate script file to store a variable, and only expose the get / set functions for that variable with export. The variable itself will not be exposed. You don't need this, because there is no class instance to refer to.

// GREETING.JS
let greeting  = "hello"
export function update(str) {
    greeting = str
}
export function show() {
    return greeting
} 

Using greeting in APP.JS

import { update, show } from "./greeting.js"

update("hey what's up")

let test = show()
console.log(test)          // <-- greeting changed to hey whats up
//console.log(greeting)    // <-- greeting variable is not available here

Just a side note: this acts a lot like a static class. You still keep a state in the greeting file (greeting = "hello"), but you can't have multiple instances of greeting.js each with their own greeting string.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.