An Interest In:
Web News this Week
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
- March 26, 2024
- March 25, 2024
- March 24, 2024
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.
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.
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.
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:
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
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To