0.5.1 • Published 2 years ago

ts-klass v0.5.1

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

ts-klass

We know what you want.

We know your colleagues don't like your pesky classes.

We know how everyone likes this shiny "functional programming" thing.

We know they get mad just seeing one new in the code base.

We know how many existing prior art out there don't even have typings (last publish: 9 years ago, what do you expect?), and we know that you and your colleagues, as fashionable people of the new age, like to keep your code strongly typed.

ts-klass does this one specific thing: providing a DSL that's both functional (what does that even mean? Well, without the class or new keywords, obviously) and strongly-typed.

How to use

You first create a klass with the klass function:

import klass from "ts-klass";

const Animal = klass({
  makeSound() {
    console.log(this.sound);
  },
});

You can then craft instances from it, as you expect:

const dog = Animal({ sound: "woof" });
dog.makeSound();

Even better, if you like how well new Animal() reads, we offer you this API called nеw.

import { nеw } from "ts-klass";

const dog = nеw(Animal)({ sound: "woof" });
dog.makeSound();

Take great care. That's not a normal e but a Cyrillic е, because (by decreasing order of importance):

  1. We have agreed how new is reminiscent of the disgusting "OO" paradigm.
  2. No-one will ever discover this if they search for new.
  3. new is a keyword in JS and cannot be used as function names.

Of course, you may need to turn off your editor's highlighting for suspicious characters. If you find nеw hard to type, maybe it's time to install a Cyrillic input method.

Notably, you can't new a klass, because we don't like new and you may get hunted down by your colleagues.

const dog = new Animal({ sound: "woof" }); // Throws error

Using nеw offers more security than calling the klass constructor directly, because it will first do a branded check to make sure Animal is a proper klass instead of any random function.

Explicit constructors

By default, the constructor returned from klass, when being called, will merge its first argument with the constructed instance. You can also provide a custom constructor.

const Animal = klass({
  constructor(sound) {
    this.sound = sound;
  },
  makeSound() {
    return this.sound;
  },
});
const cat = Animal("meow");
cat.makeSound();

Static members

You can have static members by... simply adding static before the klass declaration.

const Animal = klass({
  "static greet"() {
    console.log("Hello");
  },
});
Animal.greet();

Static methods will have this pointing to the klass body instead of the klass instance, as you would expect.

const Animal = klass({
  "static greet"() {
    console.log(this.name);
  },
  "static name": 1,
});
Animal.greet();

Extending klasses

You can use klass.extends() to create a derived klass.

const Entity = klass({
  x: 1,
  y: 2,
});
const Animal = klass.extends(Entity)({
  location() {
    return [this.x, this.y];
  },
});
const dog = Animal();
console.log(dog.location());

Named klasses can have a super klass as well.

const Animal = klass.extends(Entity)({
  location() {
    return [this.x, this.y];
  },
});

The argument of extends must be a klass constructor.

super.constructor

The semantics of super are roughly the same as in ES classes.

const Entity = klass({
  greet() {
    console.log("Hello");
  },
});

const Animal = klass.extends(Entity)({
  greet() {
    super.greet();
  },
});

Animal().greet(); // Logs "Hello"

In constructors, you also need to call super.constructor() to request the base klass to modify this. Note that we have to use super.constructor() instead of super(), because the latter is not valid in an object literal.

const Entity = klass({
  constructor() {
    this.a = 1;
  },
});

const Animal = klass.extends(Entity)({
  constructor() {
    super.constructor();
    this.b = this.a + 1;
  },
});

console.log(Animal()); // Logs { a: 1, b: 2 }

As you would expect, you cannot access this before calling super.constructor.

const Animal = klass.extends(Entity)({
  constructor() {
    this.b = this.a + 1; // Throws error
    super.constructor();
  },
});

Klass name

Unfortunately, because klass is ultimately a normal ECMAScript function, there's no great way for us to automatically bind a klass' name based on what it's assigned to. If a klass' name is important to you, you can explicitly bind a name.

const Animal = klass("Animal")({
  makeSound() {
    console.log(this.sound);
  },
});

const dog = Animal();
// Logs "A dog is an Animal."
console.log(`A dog is an ${dog.constructor.name}.`);

This can only be done once. After a klass has already been bound to a name, you can't overwrite its name by calling the constructor again. You can't assign it either—following ECMAScript semantics.

const animalKlassCtor = klass("Animal");

const Animal = animalKlassCtor("Dog")({
  // Won't work; throws error ^^^^^^^
  makeSound() {
    console.log(this.sound);
  },
});

Accessors

You can use accessors in the klass body, and they behave as you would expect.

const Animal = klass({
  a: 1,
  get b() {
    return this.a;
  },
  "static c": 1,
  get "static d"() {
    return this.c;
  },
});

console.log(Animal().b);
console.log(Animal.d);

Branded check

A klass is not an ECMAScript class (because everyone hates it). When you use klass.extends(SomeKlass) or nеw(SomeKlass), SomeKlass must be a klass constructed from the klass() function. You can check if something is a klass (and therefore can be extended or nеw'ed) with isKlass(SomeKlass).

import { isKlass } from "ts-klass";

const RealKlass = klass({});
isKlass(RealKlass); // true
const NotKlass = class {};
isKlass(NotKlass); // false

You can also use instanceof to do branded checks.

RealKlass instanceof klass; // true

Terminology

A klass is what you regard in normal ECMAScript as "class". For example, klass({ foo: 1 }) creates a klass just as class { foo = 1 } creates a class. Because klasses are directly called instead of new'ed (they can be optionally nеw'ed, though), "klass constructor" and "klass" are the same thing.

The klass() function itself is called the klass creator. Its equivalent in ECMAScript is the class keyword—you have to simultaneously provide a body, a klass name, and other metadata like extends in order to properly declare a klass.

When you write klass("name"), the return value is a new klass creator. It's called a name-bound klass creator because klasses instantiated from this creator will have names.

FAQ

Why does using this module result in a runtime error?

Although this sounds like an idea from the age of dinosaurs, this module actually uses the latest JS features. For example, Object.hasOwn is only available in Node v16.10+. If you are using it in browser, you almost always want to polyfill certain APIs.

Also, this module is literally a module: it uses ECMAScript modules (ESM) instead of CommonJS (CJS) ones. You need to import it with import klass from "ts-klass" instead of const klass = require("klass").

Can I use this in production?

If I haven't made it clear enough—please don't. A klass has much worse performance than a native class while offering all the semantics and paradigms that classes do offer. If your team wants to enforce functional programming style, please do realize that composition is a fundamentally different approach than inheritance, which klasses are built upon.

Still, this module has been fully tested and follows ECMAScript semantics (where applicable) to the best of our knowledge, so it should not be dangerous to use, per se.

TODOs

This project is still in its early infancy.

  1. Private methods/fields
  2. Interfaces
  3. Abstract klasses
0.5.0

2 years ago

0.5.1

2 years ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.4.0

2 years ago

0.3.1

2 years ago

0.1.0

2 years ago

0.0.1

2 years ago

0.0.0

2 years ago