Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Export a TypeScript class from another file than it has been defined in

For a while I've been trying to export the classes from a little library I wrote in a single place. I want each class do be agnostic of how they are made available to the outside but I can't get that to work so far.

Let's assume I've got the class Foo

// lib/foo.ts
export class Foo {}

and the class Bar

// lib/bar.ts
export class Bar {}

Now I want to make those available to whoever uses the package via the MyModule namespace and this is where it gets tricky. I don't know how to export existing classes from a namespace defined in another place than the class itself.

Obviously, this won't work because it's not valid TypeScript:

// api.ts
import { Foo } from "./lib/foo";
import { Bar } from "./lib/bar";

export namespace MyModule {
    export Foo;

    export namespace MySubModule {
        export Bar;
    }
}

I have found two potential solutions to this, but both do have their downsides. I can define a variable and assign the class to it:

// api.ts
import { Foo as MyFoo } from "./lib/foo";
import { Bar as MyBar } from "./lib/bar";

export namespace MyModule {
    export const Foo = MyFoo;

    export namespace MySubModule {
        export const Bar = MyBar;
    }
}

This will in fact allow me to create instances of the class from an external place like new MyModule.MySubModule.Bar but won't give me the possibility to use them as types since

let myBarInstance: MyModule.MySubModule.Bar;

will throw a TypeScript error stating that

Module "path/to/module/api".MySubModule has no exported member 'Bar'

The other way around I tried to use export type Bar = MyBar instead of const and while this works when saying

let myBarInstance: MyModule.MySubModule.Bar

on the other hand I naturally cannot instantiate MyModule.MySubModule.Bar since it's just a type alias.

All of this goes the same way for the Foo class.

Does anyone have any experience with this, have I maybe overlooked some other exporting feature TypeScript is providing?

like image 428
Loilo Avatar asked Mar 12 '23 14:03

Loilo


1 Answers

So Ryan Cavanaugh from the TypeScript team was kind enough to answer my question over at their GitHub.

It's actually pretty easy: Just combine both of the approaches I came up with and you're good to go:

// api.ts
import { Foo as MyFoo } from "./lib/foo";
import { Bar as MyBar } from "./lib/bar";

export namespace MyModule {
    export const Foo = MyFoo;
    export type Foo = MyFoo;

    export namespace MySubModule {
        export const Bar = MyBar;
        export type Bar = MyBar;
    }
}

EDIT 2020: Now that I have some more years of TypeScript experience, I think I should provide an additional explanation why the code above works. This may be valuable information for new-ish TypeScript developers, because it touches a topic that is not often taught: TypeScript's separate type scope.

TypeScript basically maintains a type scope parallel to the variable scope of JavaScript. This means that you may define a variable foo and a type foo in the same file. They don't even need to be compatible:

const foo = 'bar'
type foo = number

Now classes in TypeScript are a little bit special. What happens if you define a class Foo is that TypeScript not only creates a variable Foo (containing the class object itself) it also declares a type Foo, representing an instance of that class.

Similarly, when importing a name from another file, both — any defined variable and type under that name — are imported. This means that in the code above, MyFoo holds the Foo class object as well as the Foo class type from foo.ts.

So if we re-export MyFoo from inside the MyModule namespace by writing export const Foo = MyFoo, only the class object is exported (because a const only ever holds a value, not a type). Similarly, if we'd do export type Foo = MyFoo, only the class type would be exported.

So now we've come full circle: Because of the independent scopes, it's valid to export a value and a type under the same name. And in some cases (like this one), it's not only valid but necessary.

like image 55
Loilo Avatar answered Apr 27 '23 06:04

Loilo