Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to alias complex type constructor in typescript?

Tags:

oop

typescript

I am using a custom library, and for some types I have to write:

 import * as pipeline from 'custom-pipeline';
 import {MapTransform} from 'custom-pipeline';

 export const createTransform = (appConfig: IAppConfig):
   MapTransform<ISomeData, IFoo> |
   MapTransform<ISomeData, IBar> => {

   switch(appConfig.createType) {
       case 'foo':
           return new pipeline.MapTransform<ISomeData, IFoo>((data: ISomeData) => {...});
       case 'bar':
           return new pipeline.MapTransform<ISomeData, IBar>((data: ISomeData) => {...});
   }
 }

Especially the verbose constructor is irking me. I can I alias the types alright:

 type FooTransform = MapTransform<ISomeData, IFoo>;
 type BarTransform = MapTransform<ISomeData, IBar>;

Yet I cannot do:

 new FooTransform((data: ISomeData) => {...});
 new BarTransform((data: ISomeData) => {...});

throwing an error like:

error TS2304: Cannot find name 'FooTransform'.

I assume it's because I only have a type and not a class? Yet how can I alias the constructor in a way that I can do new FooTransform as above?

The definition of MapTransform looks like:

export declare class MapTransform<A, B> extends BaseTransform<A, B> {
    constructor(private mapFunc: (val: A) => B);
}

I can reduce the constructor to this:

fooMapFunction = (data: ISomeData): IFoo => {...};
new MapTransform<ISomeData, IFoo>(mapFunction);

Albeit it's not en par with a new FooTransform(fooMapFunction).

like image 755
k0pernikus Avatar asked Dec 05 '16 20:12

k0pernikus


People also ask

How do I create alias in TypeScript?

In Typescript, Type aliases give a type a new name. They are similar to interfaces in that they can be used to name primitives and any other kinds that you'd have to define by hand otherwise. Aliasing doesn't truly create a new type; instead, it gives that type a new name.

How do you use constructors in TypeScript?

The TypeScript docs have a great example of constructor usage: class Greeter { greeting: string; constructor(message: string) { this. greeting = message; } greet() { return "Hello, " + this. greeting; } } let greeter = new Greeter("world");

What is the type of a constructor in TypeScript?

TypeScript defines a constructor using the constructor keyword. A constructor is a function and hence can be parameterized. The this keyword refers to the current instance of the class. Here, the parameter name and the name of the class's field are the same.

How do I compare types in TypeScript?

Use the typeof operator to check the type of a variable in TypeScript, e.g. if (typeof myVar === 'string') {} . The typeof operator returns a string that indicates the type of the value and can be used as a type guard in TypeScript. Copied!


1 Answers

Your assumption is correct, type declarations are compile-time only, thus you can't perform any operations on them (including instantiation with new).

Solution 1: Type Assertion

Let's assume the superclass looks like this:

class MapTransform<T1> { 
    constructor(public readonly data: T1) { /* ... */ }
}

To create a specialised type alias, you can do this:

type TransformA = MapTransform<number>;

From the perspective of the superclass MapTransform, there's no difference between MapTransform<A>, MapTransform<B> and so on (that's the point of generics), so we can safely assign the constructor function of class MapTransform to a constant TransformA. Calling new TransformA() is, at runtime, identical to calling new MapTransform<number>():

const TransformA = <{ new (data: number): TransformA; }>MapTransform;

Notice the type assertion? This tells the TypeScript compiler to treat the assigned value MapTransform as an object which constructs an object of type TransformA when instantiating it with new. We can now write:

const a = new TransformA(123);
a.data.toExponential(); // works

BTW, what the TypeScript compiler actually sees is this ...

const TransformA = <{ new (data: number): MapTransform<number>; }>MapTransform;

... because type TransformA ≡ MapTransform<number>.

Be aware that all of these will evaluate true:

  • new TransformA(123) instanceof TransformA
  • new TransformA(123) instanceof MapTransform
  • new MapTransform<number>(123) instanceof TransformA
  • new MapTransform<number>(123) instanceof MapTransform

Here's the example in the TypeScript Playground.

Solution 2: Subclassing

Again, let's assume the superclass looks like this:

class MapTransform<T1> { 
    constructor(public readonly data: T1) { /* ... */ }
}

With subclassing, the solution is simple: Extend the superclass and pass the desired type parameters along in the extends clause.

class TransformA extends MapTransform<number> { }

Et voilà, you now have a constructor that works at runtime as well as a type that works at compile-time.

Unlike the first solution, the following 2 expressions will evaluate true ...

  • new TransformA(123) instanceof TransformA
  • new TransformA(123) instanceof MapTransform

... while these will evaluate false:

  • new MapTransform<number>(123) instanceof TransformA
  • new MapTransform<number>(123) instanceof MapTransform

Solution 2.1: Anonymous Subclassing

If you only need the constructor alias but not the type alias, this might come in handy:

const TransformA = class extends MapTransform<number> { };

This is called a class expression and can be used like every other expression, for example:

class App {
    private static TransformA = class extends MapTransform<number> { };

    public doStuff() {
        return new App.TransformA(123);
    }
}

More on Types

If you're interested, here's a few more links on the topic:

  • Named Types in TypeScript Spec
  • Type Assertions
  • TypeScript Handbook: Interfaces, specifically section "Difference between the static and instance sides of classes"
  • Class Expressions

EDIT

You wrote you've had issues applying these solutions to a class that expects a function as a parameter, so here's another example in the TypeScript playground.

like image 102
Fabian Lauer Avatar answered Oct 18 '22 21:10

Fabian Lauer