Volodymyr Bobyr
Volodymyr Bobyr

Reputation: 424

Shorter way to define classes in Typescript?

A standard class declaration looks like this:

class Foo {
    a: string;
    b: string;

    constructor({ a, b }: { a: string, b: string }) {
        this.a = a;
        this.b = b;
    }
}

Which requires you to copy a variable 4 times -- twice with a type! Might not seem like that big a deal, but it creates needless boilerplate for a very simple task, and is problematic with string literals, as it requires you to have hard-coded strings in multiple places.

The only simpler way i found was using an interface:

interface Foo {
    a: string;
    b: string;
}

const getFoo = ({ a, b }: Foo): Foo => ({ a, b });

But now there's need to export both the getter and the interface, which is a bit backwards.

Is there a simpler way I'm missing?

I'd love to see something like Dart's initializers:

class Foo {
    String a;
    String b;

    Foo({ this.a, this.b });
}

Update:

Was inspired by one of the answers to take a more functional approach:

interface Test {
    a: string;
    b: string;
}

export function get<T>(params:T):T {
  return params;
}

get<Test>({ a: 'hello', b: 'world' });

Which is unfortunately missing default values.

Upvotes: 1

Views: 394

Answers (2)

RAM
RAM

Reputation: 2769

You can avoid a lot of var assignment code by using parameter properties. For example, assuming you aren't required to pass an object to the constructor, you can declare the same class as follows:

class Foo {
    constructor(readonly a: string, readonly b: string) {
    }
}

Upvotes: 1

jcalz
jcalz

Reputation: 329608

One way to avoid this sort of boilerplate is to refactor it into your own library function (which might need to use a type assertion to convince the compiler not to complain)

function ifaceToCtor<T extends object>(): new (param: T) => T {
    return class {
        constructor(arg: any) {
            Object.assign(this, arg);
        }
    } as any;
}

Then instead of making a normal class, you define an interface and make its constructor with ifaceToCtor:

interface Foo { a: string, b: string };
const Foo = ifaceToCtor<Foo>(); // short!

And you can verify it works:

const foo = new Foo({ a: "eh", b: "bee" });
console.log(foo.a); // eh
console.log(foo.b); // bee

This is similar to your "getter" idea, but this one is a real class (if it matters) and uses Object.assign() to avoid the duplication of property names and allowing it to be tucked away into some library you don't need to look at.


Similarly you can use ifaceToCtor to extend a nonce superclass to add methods or other props not part of your constructor parameter:

class Bar extends ifaceToCtor<{ c: string, d: number }>() {
    method() {
        return this.c.toUpperCase() + " " + this.d.toFixed(2);
    }
}

const bar = new Bar({ c: "hello", d: 123.456 });
console.log(bar.method()); // HELLO 123.46

And this version has the advantage that what you are exporting is a standard-looking class, where the interface and constructor named Bar are brought into scope for you without requiring the interface-and-const from the first example.

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 2

Related Questions