Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 20, 2020 11:20 pm GMT

TypeScript's Secret Parallel Universe

Almost four years ago, I was a new TypeScript user, amazed by the possibilities that this freshly learned JavaScript dialect opened up. But just like every TypeScript developer, I soon ran into some hard-to-debug problems.

Animation showing a set of virtual switches labelled with "Bug" plus some number. Whenever one of the switches is toggled, a seemingly random number of other switches is automatically toggled as well.

In TypeScript land, those problems usually stem from the programmer's lack of understanding about the language itself.

I'd like to introduce you to one of these early problems I had, mostly because it is related to one of the (in my opinion) most underreported topics in the TypeScript tutorial world: the type scope. It's kind of obvious once you realize it exists, but for me it was a frequent source of confusion when I didn't know about it.

The Problem

My problem was actually very simple: I was building a library with a bunch of classes distributed over various folders.

The Visual Studio Code file tree showing three TypeScript files (Console.ts, File.ts and Stream.ts) in a folder named "Output"

For the library's public API, I wanted those classes to be exposed as a single nested object (e.g. the Console class from Output/Console.ts being available as API.Output.Console).

So I defined some namespaces, imported the classes and then I struggled. I was just not able to re-export the classes from inside the namespaces.

First attempt, turned out to be invalid TypeScript:

import Console from './Output/Console'export namespace Output {  export Console  // TS Error 1128: Declaration or statement expected.}

Maybe I need to import it under another name. Okay, second attempt:

import ConsoleAlias from './Output/Console'export namespace Output {  export Console = ConsoleAlias  // TS Error 2304: Cannot find name 'Console'.}

Third attempt maybe doing it the ES Modules way cuts it. (Spoiler: It didn't.)

import ConsoleAlias from './Output/Console'export namespace Output {  export { ConsoleAlias as Console }  //TS Error 1194: Export declarations are not permitted in a namespace.}

Fourth attempt using export const. This actually compiled.

import ConsoleAlias from './Output/Console'export namespace Output {  export const Console = ConsoleAlias}

But unfortunately, whenever I wanted to type hint something with that API object, I got the following error:

import * as API from './API'let console: API.Output.Console// TS Error 2694: Namespace 'API.Output' has no exported member 'Console'.

...but I exported that member! Why is not there!?

Hum. Maybe the export const is the problem. I should sprinkle some magic TypeScript keywords over the problem and try export type instead.

So without further ado: Fifth attempt. It compiles!

import ConsoleAlias from './Output/Console'export namespace Output {  export type Console = ConsoleAlias}

Okay, reality check: Does the type hint work? It does!

...but that joy was short-lived as well. When I tried to create a new Console instance, TypeScript errors were all over me again:

import * as API from './API'const console = new API.Output.Console()// TS Error 2708: Cannot use namespace 'API' as a value.

Oh come on.

Needless to say that I was pretty fed up with TypeScript at that point.

Video of a man throwing his computer into the dumpster

However, I did not want to believe that there was no solution to my problem, so I went to StackOverflow and, after a couple of days with no answer, I created an issue directly in TypeScripts GitHub repository.

The Solution

Ryan from the TypeScript team was kind enough to answer my question just within a couple of minutes. To me, the solution seemed pretty obvious and pretty obscure at the same time:

Screenshot of Ryan Cavanaugh's answer to my question on GitHub, stating that I should be using both "export const" and "export type" at the same time

I applied that approach and it worked like a charm.

Why It Works

Back then, I just accepted that answer and used it in my code. It sounded kind of plausible to me both, const and type, worked in some way, so I just need to combine them to make both use cases working. But there was a sense of unease in it. Why could I export two things under the same name without producing a big fat compiler error?

It took me some more months (maybe even years) of TypeScript experience to fully understand why this works, but I think that insight might be valuable for others as well, so I'll share it here:

TypeScript has a secret scope.

TypeScript basically maintains a type scope which is completely independent of the variable scope of JavaScript. This means that you may declare 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//  This is absolutely fine for TypeScript

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 the Foo class.

class Foo {}// We can use Foo as a typelet foo: Foo// We can use Foo as a constructor (i.e. a value)const bar = new Foo()

Similarly, when importing a name from another file (like we do with ConsoleAlias in the second code sample), both the ConsoleAlias class object and the ConsoleAlias type are imported.

In other words, that single name the imported ConsoleAlias holds both the class object and the type declared in Output/Console.ts.

So if we re-export Console from inside the Output namespace by writing export const Console = ConsoleAlias, only the class object is exported (because a const only ever holds a value, not a type). Similarly, if we'd do export type Console = ConsoleAlias, 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 the one above), this is not only valid but necessary.

I hope this helped refine your mental model of TypeScript.


Original Link: https://dev.to/loilo/typescript-s-secret-parallel-universe-54i6

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To