Scats: Bridging the Gap Between Scala and TypeScript Collections

Penkov Vladimir
7 min readMay 26, 2024

--

As a Scala developer working with TypeScript, you might miss the familiar data structures and functional programming concepts you rely on in Scala. scats library aims to bridge this gap by providing a TypeScript library that offers almost the same API for working with collections and options monads as you’d find in Scala, but with TypeScript syntax.

This library has several key goals:

  • Reduce Context Switching: By using Scats, you can write TypeScript code that manipulates collections and options monads in a way that feels similar to Scala. This reduces the mental overhead of switching between languages and allows you to focus on the core logic of your application.
  • Improve Code Readability: For developers familiar with Scala, Scats’ familiar API makes TypeScript code easier to understand and maintain. This is especially beneficial when working on projects with a mix of Scala and TypeScript developers.
  • Leverage Functional Programming in TypeScript: Scats promotes the use of functional programming concepts like immutability and higher-order functions in TypeScript projects. This can lead to cleaner, more concise, and less error-prone code.

Let’s delve into an example using the Option monad to illustrate how Scats works:

The Option Monad in Scala vs. TypeScript with Scats

The Option monad is a common functional programming concept used to represent the presence or absence of a value. In Scala, it’s defined as Option[A], where A can be any type.

Here’s how you might use the Option monad in Scala to check if a user object has an email address:

val user: Option[User] = Some(User("John Doe", "john.doe@email.com"))
user.map(_.email).getOrElse("No email provided")

This code snippet checks if the user option contains a value. If it does, it extracts the email address using map. Otherwise, it returns the default message "No email provided".

With Scats, you can achieve the same functionality in TypeScript, but using the lowercase option function to create options:

import { Option, option, none } from 'scats';

const user: Option<User> = option({ name: "John Doe", email: "john.doe@email.com" });
user.map(u => u.email).getOrElseValue("No email provided");

As you can see, the Scats API is inspired by Scala’s Option monad. You can use option to wrap a value and none to represent the absence of a value. The map function allows you to transform the wrapped value, and getOrElseValue provides a default value if the option is empty.

More details

I will describe the API in more details, so even those who never worked with Option monad could use it.

Why do we need Option monad? One of use cases is dealing with user input or any external data. We must remember, that data can have null values or even missing fields. Thus, if you will try to use such data as json object, you will get Cannot read properties of null or similar error.

One of the ways to solve it is using optional chaining:

obj.val?.prop
obj.val?.[expr]
obj.func?.(args)

To make final result non-nullable, we can use:

obj.val?.prop || 'default_value'

Option monad is another solution, it literally wraps the nullable value with custom object, and provide safe methods to work with that value.

Scats provides an implementation of Option monad, which is very close to scala’s variant.

Scats is available as a npm package and can be easily installed using npm or yarn. To get started, open your terminal and navigate to your project directory. Then, run the following command to install Scats using npm:

npm install scats

Alternatively, if you prefer using yarn, you can use this command:

yarn add scats

The general rule in scats is that types are written with uppercase (Option), while the constructor function is written with lowercase (option), this is because in typescript you can’t have both type and function with the same name.

Here is an example of how to create an Option monad:

import {Option, option} from 'scats';

const message: Option<string> = option('this is nullable string');

In this example we created an Option monad. It wraps the string value, which can be null. option constructor takes care of checking if the value is null or undefined .

Another options to create monad are:

  • using when. If the first parameter to when is true, then the value of the monad will contain the value, returned from the second parameter, otherwise it will be empty:
const useFullName: boolean = conf.useFullName;
const firstName = 'John';
const lastName: Option<string> = Option.when(useFullName)(() => 'Doe');
const fullName = lastName.map(ln => `${firstName} ${ln}`)
.getOrElseValue(firstName);
  • using useless. If the first parameter to when is false, then the value of the monad will contain the value, returned from the second parameter, otherwise it will be empty:
const useShortName: boolean = conf.useShortName;
const firstName = 'John';
const lastName: Option<string> = Option.useless(useShortName)(() => 'Doe');
const fullName = lastName.map(ln => `${firstName} ${ln}`)
.getOrElseValue(firstName);

If at the time of creating monad you already know, if the value is missing or not, you can use other explicit constructors:

import {Option, some, none} from 'scats';

const missing: Option<string> = none;
const existing: Option<string> = some('defined string');

Now that we have a monad, what can we do with it? First, let’s check, does it contain any value or not?

const message: Option<string> = constructMessage();
console.log(message.isDefined);
console.log(message.empty);

Both properties isDefined and empty will return boolean value, indicating if there is a value or not.

Next, we want to get the actual wrapped value. We can use get method, which returns a value or throws an Error if the value is missing:

const message: Option<string> = constructMessage();
console.log(message.get);

Nothing exciting till now, one can ask: why should I use it and wrap values, having extra performance penalty and possible error in get?

Well, using get is a very bad pattern, and in reality you should use the power of functional programming, when working with Option. First of all, we should remember that the value can be missing, so instead of using get we should use its alternative getOrElse (or getOrElseValue), which provides a fallback value:

const message: Option<string> = constructMessage();
// we can provide fallback as a constant string
console.log(message.getOrElseValue('Fallback message'));
// or as a function
console.log(message.getOrElse(() => 'Fallback message'));

If we want null as fallback, we can use corresponding properties:

const message: Option<string> = constructMessage();
const nullableMessage = message.orNull;
const undefMessage = message.orUndefined;

Now we want to do some manipulations with the value. For this we will use map function. It takes a lambda, which should return a new value:

const username: Option<string> = await loadUsername();
const greeting = username.map(un => `Hello, ${un}`);

If there was no value (so username.empty===true), then lambda will not be invoked, and greeting will also be empty. But if there was a value in username , then its value will be used to construct a value forgreeting .

What to do, if lambda may return null? In this case we need to wrap the response with another option and use flatMap instead of map :

const message: Option<string> = constructMessage();
const mapped = message.flatMap(m => option(processMessage(m)));

flatMap does actually the same as map, but it also flattens the result, so instead of getting Option<Option<string>>, you will get just Option<string>.

Another useful method is match . It will help you to do custom operations on both existing and missing values:

const message: Option<string> = constructMessage();
message.match({
some: m => processMessage(m);
none: () => processWithNoMessage();
});

Think of match as a variant of scala’s pattern matching.

Next we want to get some flow control over the value. We want to apply mapping only under some conditions.

Let’s imagine, we have user data with nullable fields:

interface User {
firstName?: stirng | null;
lastName?: string | null;
}

This interface describes external data, so we can’t control it. Both fields can be missing, can have some value or be null. We want to construct greeting with first name, if it is defined and not blank (i.e. not ""), otherwise we will use just Hello . To check the value before using it, we can use filter method:

const user = await loadUser(userId);
const greeting = option(user.firstName)
.filter(n => n.length > 0)
.map(n => `Hello, ${n}`)
.getOrElseValue('Hello');

Take a look, inside lambda of filter method we know, that the value is not null or undefined, so we can safely use .length.

If we want to use either first name, or last name we can use orElse method (which takes lambda) or orElseValue method (which takes a constant), which similar togetOrElse, but allows you to stay inside monad:

const user = await loadUser(userId);
const greeting = option(user.firstName)
.orElse(() => option(user.lastName))
.filter(n => n.length > 0)
.map(n => `Hello, ${n}`)
.getOrElseValue('Hello');

We use orElse to avoid unnecessary creation of option for lastName.

Finally, we can test, does the monad contain the value, that we want. There are 3 methods for this.

  • contains checks, does the monad contain specified constant:
const user = await loadUser(userId);
const firstName = option(user.firstName);
if (firstName.contains('admin')) {
console.log('Someone tries to login with admin privileges');
}
  • exists checks, does the existing value matches the lambda:
const user = await loadUser(userId);
const firstName = option(user.firstName);
if (firstName.exists(n => n.length > 3)) {
await storeUser(user);
} else {
throw new Error('first name is missing or too short');
}
  • forall checks if the value matches the lambda, when exists, or is missing:
const user = await loadUser(userId);
const firstName = option(user.firstName);
if (firstName.forall(n => n.length < 3)) {
throw new Error('first name is missing or too short');
}

There are other methods in option monad, such as toCollection , mapPromise, toRight, toLeft, which will be covered in next articles, because they require to learn monads such as collection, either and others.

Alternatives:

  1. ts-option: Scala like Option type for TypeScript/JavaScript.
    This is amazing library, that expired me to write scats. It has only Option monad. https://github.com/shogogg/ts-option
  2. fp-ts is a library for typed functional programming in TypeScript. It allowes you to chain operators with pipe as in rx-js. https://github.com/gcanti/fp-ts
  3. ramda: a practical functional library for JavaScript programmers. https://ramdajs.com/

Links:

  1. scats on Github: https://github.com/papirosko/scats
  2. scats on npm: https://www.npmjs.com/package/scats

--

--