Functions and algorithms implemented purely with TypeScript's type system
github.com
github.com
Did anyone ever think "Hang on, maybe the problem, the reason why coding is so difficult is that interfaces are often too darn complicated to keep track of? Oh, wait a minute, if the interfaces were really simple, would we even need TypeScript at all? Why don't we just design our software in such a way that the interfaces between components are simple so that we can easily remember which functions accept what kinds of input without losing our train of thought? Wait a minute, this idea already exists? It's called loose coupling and high cohesion. How in Alan Kay's name did I not think of it before???"
TS is complicated and chaotic because the language it compiles down to is. TS' aim is to model the behaviour seen in JS in a flexible, user-friendly type system. Good luck!
What doesn't make sense to me is when teams (like mine) have a blank slate and the choice of any mainstream backend language, and then opt in to that ecosystem anyway. TS is good when you're coming from JS sure, but some of us have used more "sane" languages like C# and Go :(
This is sarcasm, right? The creator of TS is the lead architect of C#
C# is in some ways less shackled by the runtime it has to target, while TS compiles away entirely. The CLR can be changed to suit .net/C# needs, and JS is not under their control.
I would have gone with C# or Kotlin if I had the luxury of choice, but I'll get that with a deliberate career move maybe.
That way you can define the type in a single place, have runtime validation for that type, but also be able to use the type in typescript at compile time.
Not exactly sane. Go is one of the most clunkiest awkward languages I've ever worked with. The only benefit is that it's somewhat easy to learn.
It's like a modernised C but for web/infrastructure. Unfortunately ignores about 3 decades worth of progress in other languages, and is simplistic to a fault. Jumping into an unknown codebase is easier than every other language I've used as a result.
As you've said, Go is sane by some definitions of the word but a user should know what types of sanity and insanity they are willing to work with and which are inherent to their problem space
I suspect what you mean is that they aren't just compiling down to JavaScript, but they are wanting to do so in a way that there is no foreign function interface (FFI) consideration for folks in either language. That is, largely to say, they don't want a Typescript runtime?
> 3. Impose no runtime overhead on emitted programs. > 4. Emit clean, idiomatic, recognizable JavaScript code. > 7. Preserve runtime behavior of all JavaScript code. > 8. Use a consistent, fully erasable, structural type system.
https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi...
I hear this regularly but don't think it holds water. Other statically typed languages compile to any number of targets without issue; machine code, llvm, and even JS.
Type annotation work is definitely the annoying part which slows me down and breaks my train of thought.
That said, I don't dislike TypeScript in theory as a language if used carefully. It may just be worth the annotation effort (kind of like how comments are usually worth the time they take to write). I just don't think it's worth the additional transpilation step and all the compatibility and source mapping issues that come with it.
This is akin to throwing water over an oil pan fire, then being frustrated at the water for your house burning down.
You need to either use the Typescript way throughout your dev process, or not use Typescript. Both of those options are valid, but you're currently doing half of both, resulting in unnecessary frustrations.
A better metaphor would be; I want to boil some chicken and I have a pan full of boiling water... Then just as I'm about to put the chicken inside, the head chef stops me and tells me that I need to pour oil into the pan.
I protest; "But sir, there's no need, the customer asked for boiled chicken, not fried chicken..."
"Do as I say" says the head chef.
So I reluctantly pour a tiny bit of oil on top of the water, then I add the chicken. It splashes around a bit, no big issues, and the chicken comes out OK.
The customer got the boiled chicken they ordered, and they're satisfied.
"See, it all worked out... Aren't you glad you listened to me?" says the head chef.
If you start by just throwing all the crap you want into a pot and afterwards try to remember what you added and how long you cooked it for of course it's going to be a lot of extra work. TypeScript requires a methodical approach which over the long run makes it easier for the entire kitchen.
But parent is saying that you should recognise that you're opting yourself in for a harder time by ignoring typescript at the beginning, and then trying to retrofit your way into type safety. That's definitely going to be more difficult and frustrating.
So either don't use typescript (which is fine fine - no one here cares about the programming language you use), or, use it 'properly' from the beginning and work with the tools you're using rather than against it.
It sounds like your process is a bit of a drag on you and I think you should improve it.
Anti-patterns are the same in JS and TS; tight coupling and low cohesion is bad, complex interfaces are bad, unclear separation of concerns is bad, poor encapsulation is bad.
So maybe, TS working as intended
Also depends on your TS config probably, but TS does a pretty good job with inference, so I find that I don't need to write that many type annotations, especially when assigning variables
TypeScript is like training wheels on a bike. All good if your priority is not to fall, but if you want to compete in the Olympics, you may have different priorities.
Also, I think starting with JavaScript and migrating to TypeScript leads to much better code than just starting with TypeScript. I think the reason is because if you start with JavaScript, you naturally tend to avoid architectural complexity because it can quickly become unmanageable. So then when you add type annotations to existing JS code, you're not adding extra architectural complexity; just adding types.
When you start directly with TypeScript, it allows you to reason about much more complex interfaces so there is more temptation to over-engineer architecturally. Devs feel more free to invent all sorts of unnecessary abstractions which will come back to bite them later.
Spaghetti code is spaghetti code; you can label each noodle but it's still spaghetti.
You might not get any value from the typing when writing the code, but the poor sod tasked with maintaining it two years later definitely will.
Also, the added types and compile time check will greatly benefit anyone trying to perform all but the most trivial of code refactoring.
Typescript adds some inertia initially, and you are correct in that it will allow inexperienced developers getting away with writing overly complex code, but it's definitely worth it for any code base larger than a couple thousand LoC.
Good abstraction would imply that you're dealing with a different, more abstract type as you move up the component/dependency hierarchy. If the same type (especially a complex type) is present at many layers in your code hierarchy, it often means that the abstraction is leaky.
A common issue I often see is when devs try to pass a Logger instance to all components and sub-components in their app... Instead, why not just make all the components emit events (or invoke some kind of listeners/callbacks), then handle and log all the events at the root of the code in one place? This is a lot more flexible because then you can use any off-the-shelf generic component and it doesn't need to know about the existence of your Logger.
Also, it's a lot easier to read and maintain code if all the logging is invoked in one place. Logging is a single concern, so ideally, it should only affect a single component/source file.
Typescript is going to help you out greatly there. Nothing about any of that will slow you down, in fact it'll do the opposite. If you're leaving those all as `any` until the last minute I think you're really leaving a lot of productivity on the table.
TS is lauded, but I really think it's just the best of a bad bunch.
But at the time, many complained of the lack of jQuery support and needing to transpile to support other browsers, so it didn't get the interest it deserved.
If you're writing software and you're consuming libs and frameworks, a fragmented ecosystem sounds less than ideal.
And then everyone replies with, “well that’s not _real_ OO - if you did OO right the software would be perfect!”
Except real OO cannot be done.
So, yea types are a tool and should be used wisely. But let’s not pretend like there’s some magical methodology out there that removes complexity from software.
Ironically the separation of state away from logic is why backend web development tends to be easier than say game development.
>> but now many developers are actually using it as such on real projects
Seriously? That sounds like being bored and overpaid and/or not having enough pressure to make actual web and implement new features.
I get that typescript's decision was to model js's behavior exactly in the type system. But now having seen the costs of that and how much can be gained by relatively small constraints on js I think it was a mistake.
- Brian Kernighan
I agree with the notion that simplicity is hard though.
If you are using typescript you can either go down the path of using `as` and `any`, which at that point the benefits start declining. Or you can work really hard with typescript and make something complicated that correctly matches your code's logic.
But saying, "write simple code" doesn't really help much without saying how to do it.
Good luck in defining 'simple'. For me, a simple function only accepts inputs which it knows how to handle, and performs no side effects. Bonus points if it always terminates. Personally I cannot maintain this level of simplicity without a type checker.
But on the plus side, you're still allowed to remember what your functions accept! The checker only kicks in if you get it wrong.
Haven't brought this up in a while. Ya. I feel coding is hard due to complex interfaces. I once worked on a programming abstraction, mechanisms, aiming for a consistent interface for easy composition.
Key features of mechanisms were:
1) All data types were mechanisms
2) A Mechanism needed no context (parameters) when invoked.
3) Composition of a mechanism could take any number of mechanisms (primitives at this point are treated as mechanisms) and return a mechanism.
4) Upon invocation, a mechanism functions in one or more modes, returning either a primitive data type or another mechanism.
Think of it as currying or Lisp's S-expressions, but supercharged, envisioned as a base for Visual Programming Languages.
An example of composition using mechansisms:
// Compose
addTwo = print( map( add( 2, emitFromRange(0, 20, 4) ) ) );
Which is saying "Print a mapping of adding 2 to an emitted range from 0 to 20 by 4."
// Invoke
addTwo()
If you want simple interfaces, just write simple interfaces. TypeScript does not prevent you from that. But it makes complex things possible, when you need it.
If you work with developers who like to make things needlessly complex, the solution is not to make the tools less powerful.
For those of a less elevated intellect, I think the answer is that becoming Turing Complete is actually a very low bar. Once you've got recursion and pattern matching I'm pretty sure it just drops out in the wash. "Type checking is unification" (some computer scientist or other, possibly Luca Cardelli)
This is my "lodash for types" project. It supports integer division for up to 2^64, for example. It has over 200 composable utility types.
lodash does come to mind in fact. The last few shops where I've worked use a very small subset of lodash utilities and ignore most of the library. Their documentation is terse so it's often easier to just write your own code that is longer but more readable to the average engineer.
There is a project I'm working on to eliminate the latter bit - ideally, a way to represent these complex types with "zero type-level code"
These projects are interesting in an amusing "oh look, another Turing complete type system" way, for the author to gain expertise, and as something to highlight in CVs and to chat about during interviews. But other than that, I would never actually choose to use any of this in a real-world project. Depending on the stability of this API (TPI?), and subjecting colleagues to esoteric libraries would be a nightmare to maintain and support.
This can be frustrating in the moment sometimes, but other times a type can do two really powerful things:
1. prevent someone you’ve never spoken to from doing something legitimately dangerous 2. provide browsable documentation about the constraint its enforcing and how other places in the code resolve the constraint.
It doesn’t always work out that way, sure, but that’s the ideal types are striving for.
It doesn't provide "browsable documentation". It provides complex unreadable types that you have to painstakingly manually "compile" in your head to figure out what the hell is going on
You don't have to compile in your head; you can declare a variable or assignment with some type property and see where an error is thrown.
I've seen quite a few cases where IDE's type expansion (whether on hover or in type errors) would result in a dozen lines of nested types. Worse than no documentation at all.
(And, of course, as with other source code, illegibility implies more documentation needed. For type systems in particular, I think people tend to under-declare their types [i.e. they'll do "Record<KeyType, Array<{name: string, address: string}>" instead of "Record<KeyType, EmployeeRecords>"], which gives the compiler fewer hooks to shortcut talking about a complex type).
There is one thing consistently true about type systems that frustrates me: I write in procedural code all day, but basically all type systems are functional code. It is frustrating to have to spin my reasoning around to attack a problem in a different language structure when I've been doing straight-line statement-by-statement all day. I don't have a solution to that, but I observe the challenge.
None of these actions make types documentation. The necessity to do this makes them a hindrance at best.
> I think people tend to under-declare their types [i.e. they'll do "Record<KeyType, Array<{name: string, address: string}>" instead of "Record<KeyType, EmployeeRecords>"
Oh god, so true :)
By the same argument, code isn't documentation. And yet it is; all manner of style guides will tell us "Document the non-obvious, but let the code speak for itself when it's obvious." The challenge, of course, is obviousness is subjective. And I've definitely encountered type stacks where the author thought they were being perfectly clear and the reader didn't. But that makes it no different from other forms of docs.
I think the truth is types (and code) are a little of both documentation and implementation. After all, if code alone were sufficient, we'd just be hand-rolling 1 and 0 patterns to tickle the CPU directly; the existence of programming languages themselves are to bridge the gap between the minimum information a computer needs and human comprehension of what we're trying to get the computer to do.
(Whether types are docs at all, let me put it this way: every modern style guide I've found for languages where strong static typing isn't available declares I have to put the types in the documentation, especially for function calls and the like. That seems to heavily imply that types are a form of auto-checked documentation).
It isn't.
> all manner of style guides will tell us
A lie parroted ad infinitum doesn't make it truth.
Comments clearly aren't code, in the sense that they're a specific form in a language dedicated to being thrown out by the compiler / interpreter / etc (usually; I'm handwaving over the metaprogramming approaches like Doxygen that parse comments). But there's a reason we name variables `time_to_completion` instead of just `a`, or name functions (or have functions instead of jump pointers crammed into global variables), etc.
That's a pretty [good definition I think](https://en.wikipedia.org/wiki/Documentation). I also think a type does provide explanation and instructions about how to use typed code. So, I think by definition, types are undeniably documentation.
- types are not documentation
- tests are not documentation
- code is not documentation
Note that there are very few languages that have those kinds of types. Usually it's enums and union types that usually tell you nothing about where to get those ranges, why those ranges exactly, or what those ranges mean.
Literally right now I'm looking at a GraphQL schema that tells me that for a collection of lists of data it returns the following list types: Generic, GenericV2, Onboarding, OnboardingActive... That's it. Actual call returns only a few of those with no apparent difference in actual data.
It is typed though.
The type vs. no type argument is basically: what's the cutoff line for constraints that are worth encoding as types or not?
It would be funny to see web servers running the typescript compiler (type checker) instead of a JS runtime, though.
In cases where the type system can encode the invariants you care about, it can be worth the extra complexity so that you have code that has compiler enforced correctness (at least in some aspects). Of course, you can't do this for any invariant in a given type system (eg lifetimes are not representable in Java). When it is possible though, debugging a type system has the advantage that it's done on your local machine, away from prod.
Probably stupid question, but that probably means that one could, theoretically, implement a type system for any one of those DSL(-like) type systems, isn't that correct? And so on and so forth, we could have, theoretically, DSL-like type systems all the way down.
The most generic one I know of is Idris. It uses some heuristics to make some functions decidable that in theory should not be.
Eventually we need to make type systems just metaprogramming using the primary language that they're intended for. There's no reason for those to be two languages.
The issue with meta programming is that it’s too powerful, in the sense that the transformations can’t be statically checked for typing rules.
It doesn't matter how flexible the macro programming is, if it reduces statically to something you can typecheck, therefore this artificial segregation of syntax and rules (and mental models) represents us solving a problem superficially, in an almost cargo-cult way, because we never stopped long enough to think about at depth.
I think the issue is that the vast minority of logic is statically checkable, so your type-level logic would have so many weird restrictions that you couldn't use the full syntax anyway. So it's actually beneficial to keep them separate.
Nim is like this and its fantastic. None of the weird special rules for metaprogramming, just Nim code manipulating ASTs at compile time procedurally. Can even use standard library.
I'd love to know if anyone could reproduce the N-queens example in Nim: https://www.richard-towers.com/2023/03/11/typescripting-the-...
I believe it is possible, but don't have the time to try it out.
> The Nim compiler includes a simple linear equation solver, allowing it to infer static params in some situations where integer arithmetic is involved.
From: https://nim-lang.org/docs/manual_experimental.html#concepts-...
It doesn't have to be that way and in my opinion for general purpose languages it shouldn't. The type system should be about constraints that facilitate reasoning about your code - reasoning by machine and human.
Compile time computation should be a separate facility.
(That a lot of that was to facilitate reasoning about nearly untyped JS code doing a lot of dynamic code things is, depending on which side of the deliberation you are on: 1] a sign that dynamic code has always been that complex and developers have had to do all that sort of "compile time logic" in their own heads for so long, and/or 2] a sign that Typescript's "flaws" come from trying to be too supportive of existing bad JS code.)
Most of them can be coerced to do general computation, especially if they have things like if-branch, and some sort of self-reference.
But this is only true for certain languages. Overall most type systems are not Turing complete and therefore not suited for general computation
https://www.richard-towers.com/2023/03/11/typescripting-the-...
(Edit: this is mentioned elsewhere in this thread by Kerrick)
Zig strives to use the same syntax for metacomputation, although its approach is not tightly integrated into the type system, which is why you'd see compilation errors at expansion, not function definition.
My goal was to pass the Typescript types to the JS runtime, maybe there's a better way and GPT-4 just led me astray...
[0] https://github.com/itsdouges/typescript-transformer-handbook
https://github.com/ronami/meta-typing/blob/master/src/utils/...
e.g. :
type zero = []
type Inc<N extends string[]> = [...N,'']
type Dec<N extends string[]> = N extends [...infer T,''] ? T : never
type one = Inc<zero>
type two = Inc<one>
type oneb = Dec<two>Another approach which I tried is to do arithmetics on digits directly, storing digits in a tuple instead, but the code is not as elegant as the tuple one
https://github.com/dqbd/ts-math-evaluate/blob/main/src/math/...
Once we annihilate all human life on earth, the stock market will be at $0.