Consider
const fooType = generateTypeInfo!<Foo>();
Where `generateTypeInfo!` is a macro that expands to a JS object encoding the “Foo” type (e.g. if `Foo` a record type, `fooType` will be a record with its encoded field types). It still compiles to readable JavaScript.What this macro does break is that TS = JavaScript with type annotations, and all you need to do to compile is remove those annotations (excluding enums, but they are all-but-deprecated and obsoleted by string unions) without even type-checking. Since now you also need to expand the `generateTypeInfo!`, which requires actually computing the structural type of `Foo` (and if you want any sort of nominal type metadata, that too).
And that’s still a problem because type-checking is slow, but removing the annotations is fast. If there’s a limited way to resolve types for these annotations which restricts the resolution scope, that would be a good candidate.
—-
There's also more issues with TypeScript having a separate runtime besides it not being JavaScript. TypeScript types are structural, so they currently are available at runtime: to determine an object’s type, inspect its structure. Anything more and you quickly run into non-trivial cases being literally impossible: if you want erased nominal information like whether a string is part of a string union, TypeScript’s type system is Turing complete; and if you want the nominal type name, implicit structural conversions mean that once a value leaves its annotated cast or definition it’s effectively undefined.
However, such a preprocessor will basically need to re-implement TypeScript's type checking. So either TypeScript must expose it via an API, or the preprocessor needs to itself implement a subset (which as mentioned, could also be faster)
const fooType = getType<Foo>();
Which is exactly what you wanted, just as a plugin to TypeScript rather than built into the official compiler.But in all seriousness you can gain reflection without a runtime. Just expose the types as data.
Seems foolish to not do something like this. Particularly when you look at the lengths so many devs have gone to try and replicate it.
For instance with Generics in Java a List<String> is the same as a List<Integer> at run time, the compiler can enforce rules that let you add a String to one but not add a String to the other but the runtime has no idea. This has various negative consequences but it also let them retrofit generic collections on top of the old collection implementation in Java unlike the disaster in .NET where both had to coexist for years.
Similarly C compiles to machine code and in machine code there are just memory locations and registers, types are implicit in how you use those things but not spelled out explicitly. C++ does have RTTI but is one of the many open pits, like Exceptions, in C++.
Typescript types are the same way, once the compiler has done its type checking you know the code is going to behave according to the type system even if the types have been "erased".
If you make an ArrayListX that is not generic but extends ArrayList<X> it is possible to see the type parameter that was extended, the same is true for implementations.
There are other kinds of type unerasure that you can use w/o the language supporting it explictly, such as you could pass the type parameter X.class into the constructor and put it in a field so that you could call some method like getContainedClass() and get a copy of X.
Now the introspection/reflection API does have support for talking about parameterized generic types that doesn't mean that information always exists at runtime.
I worked on a crazy project where I did a lot of fighting with ordinary types and parameterized types and unerasing types by rewriting the names of methods to avoid conflicts, if you have methods like
A doSomethingTo(B)
A doSomethingTo(C)
you can rewrite these to Expression<A> doSomethingToB(Expression<B>)
Expression<A> doSomethingToC(Expression<C>)
but you can't have an overload that takes two differently parameterized expressions, this lets you write Java code in a lisp-like syntax that can be metaprogrammed on:https://github.com/paulhoule/ferocity/blob/main/ferocity-std...
that project got me thinking a lot about the various ways types manifest in Java and I made the discovery that when you add Expression types you practically discover an extended type systems where there are many things that are implied by the existence of ordinary Java types.
I like Venkat's presentations: https://www.youtube.com/watch?v=34oiEq9nD0M
BTW. I do not agree with the opinion in the topic. Since Typescript is compiled to something else (usually JS), I'd prefer Typescript to have proper annotations (there are "decorators") and compiler hooks and just use code generation for concerns like (de)serialization.
The fp-ts, io-ts author Giulio Canti has decided that the best way to proceed was to merge the two communities of fp-ts and effect, even though it's his work that is mostly known with effect being much more niche.
It's essentially io-ts squared in its powers. The amount of things you get from schemas are endless, not just decoders and encoders but also apis, lenses, constructors, and many other things.
It's obviously heavily fp-leaning, albeit I would say effect systems such as effect, based on Scala's ZIO, are much more approachable than haskell/purescript-like languages.
> 3. Impose no runtime overhead on emitted programs.
> 9. Use a consistent, fully erasable, structural type system.
It's also explicitly called out as a non-goal in that doc:
> 5. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
> 6. Provide additional runtime functionality or libraries. Instead, use TypeScript to describe existing libraries.
[0] https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi...
In this way there is no runtime overhead (point #3) -- it's pure data to be optionally consumed. They wouldn't have to prescribe any particular reflection module (point #6).
The only point this would violate is #5 but that is, of course, the point. We want them to reconsider because it's such a powerfully useful feature. And people are doing it on top of and outside of TypeScript where TypeScript would be best tool to give this information for TS users.
Today, you can already write a program using the TypeScript API to inspect all of this information to accomplish whichever scenario is at hand. The existence of all these tools, each with different opinions on design direction and implementation trade-offs, demonstrate that this is possible. Yet it's not demonstrated how those tools would benefit from reading from a data file as opposed to using the TypeScript API.
Something like api-extractor has different constraints from io-ts which has different constraints from typescript-schema. The API exists today and is apparently sufficient to meet all these tools' needs, yet what's proposed is a data file that can encapsulate any possible operation you might perform with that API.
Having TypeScript try to imagine, implement, and maintain a data file format that can satisfy _all_ of these tools' use cases, plus any future tool's use case, is a tremendous effort with no clear upside over using the existing API.
It's easy to gain support for the idea of "put the types in a file" because anyone reading that can imagine a straightforward implementation that just achieves their particular goals, but everyone's goals are different and the amount of overlap is not actually all that high when you look at the wide breadth of tools in the list. There's a very different amount of information you'd need to enable a type-aware linter (which is basically type info on every expression in the program!), as compared to a simple documentation generator (which might only need top-level declaration info).
TypeScript: We can write our own type file format!
Situation: There are 15 competing type representation formats
Though while there's a lot of type-data tools, are there actually a lot of type-data formats out there right now? I've been using zod, and it doesn't have one. You have to use code to tell it about a type. Someone did make a library to let zod load types from pure data, but the format used there is... typescript source code.
Yes, but also no. The point is that it doesn't really add more semantics over plain JS. You can erase all the type annotations (well, replace with "any") and it'll do the same thing if you ignore the type checking.
If there's any "transformation" from TS to different-looking JS[0], it's mostly just syntactic embellishment (think LISP macros and such), not semantic.
[0] Not sure if there is, tbh
In every workflow I've seen, TypeScript is already a language that gets compiled to JS. It might as well take full advantage.
To make dynamically typed languages nice for bigger projects we have typescript for type annotations (it's does not make it a statically typed language!). It is there, it is used, it scales and it works. If your type script project "collapses entirely", it's probably not the languages fault.
what is nice about a dynamically typed language? like, seriously, can you list some reasons for a dynamically typed language to exist?
With dynamic languages you can just code, without being held back by a OOP type system, but it can get very complicated to understand what the typed of you parameters and return values actually are.
Typescript gives you the best of both worlds.
"Dynamic Languages have fewer language limitations Less need for bookkeeping objects and classes Less need to get around class-restricted design. Study of the Design Patterns book: 16 of 23 patterns have qualitatively simpler implementation in Lisp or Dylan than in C++ for at least some uses of each pattern[...]"
Typescript isn't a runtime on top of JavaScript, but it very much is "a new language that compiles to JavaScript".
This means that you can run and test code that doesn't typecheck yet -- very useful during development. Also, production builds can run the typechecker in parallel with other build steps. That's how we optimized our build system at work.
But essentially, it's a design choice by the TypeScript team. They've decided that TypeScript shouldn't add runtime features on top of JavaScript, but instead should simply type JavaScript, and I think this has contributed to the success of TypeScript.
I just finished removing them from a major project. Here’s a few of my personal notes:
1. They don’t play well with duck typing. (Eg. show me a subset of an enum)
2. They require an import every time you want to utilize them.
3. They are pretty wordy compared to union strings.
4. Their string value version encourages misuse as a key-value pair.
5. Unless you use the string value version, they suck to debug because logs just show 0,1,2,3.
There are too many ways to accidentally import/redeclare/rescope the Type of an enum so that TS "knows" the Type, but because that type (generally) has the same "name" as the most likely (Locator) Instance it assumes the same access applies leaving runtime errors behind when that Instance isn't actually imported/available. Typescript has no easy way to tell the difference between access to the Type isn't access to the (Locator) Instance (nor vice versa). Reasoning about those runtime errors or preventing them is additionally tough for people too because of the same "name" problem for two different things.
This is something that's painfully hard to avoid in cases where you are trying to encapsulate an API's Types separate from its imports/exports because they might be introduced or manipulated at runtime (plugins, sandboxes, proxies, etc). Unfortunately, this is also too easy to accidentally do even when you aren't intentionally doing something complicated like that (trying to generate automated .d.ts files in a bundling toolchain, for example, when APIs are in the boundary space between unintentional public API and internal tree-shaking or optimized symbol renaming).
enum CheckboxState { On; ParentOn; Off; }
Becomes
{ [0]: “On”, “On”: 0, [1]: “ParentOn”, “ParentOn”: 1, [2]: “Off”, “Off”: 2 }
So things like Object.keys give bizarre results. It’s done this way so you can use the name or the value as an index.
IME, most of the complaints about enums apply only to numeric ones.
The major exception to that AFAIK is the fact that enum members of any type are treated as nominally typed (as in A.Foo is not assignable to B.Foo even if they resolve to the same static value). I am among the minority who consider this a good thing, but I recognize that it violates expectations and so I understand why my position isn’t widely shared.
If you want a statically and runtime typed language in the browser, you should not use typescript. Use Kotlin, Rescript, …
As such, RTTI would make typescript go from a compromise language (we use it because we are stuck in the JS ecosystem, not because it's a good tool) to a legitimately useful backend language (We use it because it has the right features for the use-case)
If I could choose what language we use on the backend, I would leave the JS ecosystem entirely and write everything in .Net. But it's difficult to find .Net developers since every bootcamp these days produces react developers
It would be best to abandon javascript and typescript as backend languages. I don't mind them on the frontend because a typescript compiler is useful when the application you're writing is originating data and not receiving it. And javascript does DOM manipulation admirably. However, we live in a world where humans are more expensive than computers. If I had my way, we'd write C# backends and Angular/Typescript frontends, but that's not the world we live in. Everything has to be JS because most people employed as "software engineers" are not truly very good at their jobs
There are many languages that compile to JavaScript. Pick one of them and use it!
And I have the feeling, that people who want runtime typed Typescript would rather like to write Java/OOP style code instead of JavaScript. But JavaScript is a dynamically typed language, and that's also nice. Be happy with what you have!