Hacker News new | past | comments | ask | show | jobs | submit login
Functions and algorithms implemented purely with TypeScript's type system (github.com/ronami)
166 points by behnamoh on July 5, 2023 | hide | past | favorite | 124 comments



I don't know whose brilliant idea it was to turn TypeScript's type system into a Turing-complete language but now many developers are actually using it as such on real projects and it's a massive productivity drain and it makes some code impossible to read... Often, the IDE can't even display the type when you hover over the variable because it's too darn complicated.

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???"


If the Typescript team got carte blanche to create their own language, it would likely be... C#.

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 :(


Go is sane? I just started using it and the parsing libraries all standardize tags and reflection.

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.


Go is sane for some definitions of the word! If I got to choose a language to work with though, it would definitely be Kotlin or C#, but I prefer Go over Python because there are fewer ways to solve problems, leading to generally simpler code.

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.


Go has it's time and place just like C. The developers chose to ignore that progress and created the C for distributed and parallelizeable code. Using go for what you use JavaScript for is like using C to do Java work. Sure you can do it, but it's not going to be easy and you're going to have a bad time.

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 come from a Java / Kotlin background and TS is totally fine.


Some quirks I've noticed are typed objects not performing any validations, like for building DTOs. Structural typing so the newtype pattern is not possible without some awkward hacks. A general "disconnect" between compile time and run time behaviour, so once you start casting your errors are relegated to property access/usage time, which makes — IMO excessive — test coverage paramount in catching bugs wherever you have conditional logic.

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.


If you’re taking in data from an external source that needs to be validated, I highly recommend a schema library like zod, that can produce a typescript type.

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.


Can you use something like Zod with the newtype pattern? I use DDD-style types for domain specific data over primitives.


> TS is complicated and chaotic because the language it compiles down to is

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.


This feels like an odd excuse moreso than a reason. Consider that most folks consider compiling software to go down to an assembly language. Which often has crazy semantics and intrinsics that many of us don't understand or know about.

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?


Precisely, TypeScript only exists at compile time. Half their goals relate to this fact, which leads to constraints regarding what the language can achieve.

> 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'm curious on reasons why not to build a new runtime. That it is hosted in javascript seems like something that shouldn't be that big of a deal.


> If the Typescript team got carte blanche to create their own language, it would likely be... C#.

This is sarcasm, right? The creator of TS is the lead architect of C#


I feel like that’s supporting the parent, right?


Not really, because it's not a question of if TS creators got to make their own language.


And C# iterates at a rapid pace, with lots of language features being added, some of which would would not be possible in TS.

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.


A goal of TypeScript was that it could be added incrementally to already existing JavaScript code. This meant it has to support a wide variety of crazy pattern used in already existing JavaScript frameworks and libraries.

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.


If you have freaky type-level metaprogramming in your project (even in a dependency) I think it just means people are playing around. Someone did that stuff for probably more for fun than to solve a real technical problem.


To be fair it’s fun to write ridiculous types to get ridiculously specific intellisense


Typescript is perfectly fine as a nice way to clarify what you're trying to write in Javascript - and it's much more pleasant, coming from more strictly typed languages, to deal with TS. But golf should only go so far. I draw the line somewhere around ternaries being used in type definitions. Like, shut up and actually write the function body, I'll figure out what it does.

>> 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.


It's also bad because you have to, as a programmer, actually type out the types a lot (compared to say, Ocaml, Haskell or Rust), and like you say, it makes it impossible to read.


Personally, I don't get much value out of TypeScript based on how I structure my code. I usually use 'any' types everywhere until my code is fully working and then, just before I open my PR, I have to painstakingly define types and replace all the 'any' references everywhere... Then whenever I get warnings while doing this, it's always related to me messing up the type definitions themselves and not related to my code. It feels like the type system is the error-prone part and my code is the reliable part... Should be the reverse; types are supposed to help me write my code but I find that it's my code (simple interfaces) which help me write my types.

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.


>I usually use 'any' types everywhere until my code is fully working and then, just before I open my PR, I have to painstakingly define types and replace all the 'any' references everywhere.... It feels like the type system is the error-prone part and my code is the reliable part

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.


I disagree, my point is that my code is better if I write it as JavaScript. TypeScript is more like an extra unnecessary thing I need to do which doesn't add any value.

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.


This metaphor is all wrong. If we're going to stretch cooking, TypeScript is the recipe book. While cooking it seems you would prefer to write "add a bit of something savoury", when you really mean "1 tbsp Worcestershire". TypeScript's greatest strength is in the next person being able to pick up the component and being able to run with it, or you being able to replicate it again in three months.

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.


In what ways would writing things the Typescript way make your code worse than the JavaScript way? I can't imagine the typing system preventing me from doing things that are better, other than cases where I want more expressive typing itself, which isn't even applicable to JavaScript.


Maybe your code is ok and understandable to you when you first write it, but when someone else has to work on it a month or two later, a lack of types makes it significantly more difficult.

It sounds like your process is a bit of a drag on you and I think you should improve it.


Completely valid - if you don't see value in a tool, don't use it.

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.


If you replace axe with chainsaw, you can’t expect it will perform better if you keep the same workflow of smashing the thing into the tree. To get the advantages you have to adjust your workflow, otherwise it will of course feel like an obstruction.


It's not the same because maintainable JavaScript code essentially always converts to maintainable TypeScript code. Adding type annotations to existing JavaScript code won't make it harder to maintain. However, maintainable TypeScript code will not necessarily convert to maintainable JavaScript code after removing the type annotations... Yet as well as TS can hide the tech debt and delay repayment thereof, it's still tech debt and you still have to pay interest on 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.


Whenever I've faced this problem (usually converting legacy JS to TS), it's because the original code was structured in a complex or unique way. Sometimes it's unavoidable, but for most code, refactoring it to make it simpler and easier to read also simplified the typing for the code

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


I agree with that but the way I see it is that if your JS code is bad, then you may get value out of TS, but if your JS code is already good, you won't get any value out of TS.

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.


> ... the way I see it is that if your JS code is bad, then you may get value out of TS, but if your JS code is already good, you won't get any value out of TS.

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.


It also lets experienced developers who are inexperienced with the code base become productive faster. It's a boon for anyone who hasn't been there since the start.


I would argue that if you are writing a lot of ‘any’ types before writing types that you a very likely doing Typescript wrong. The vast majority of you code (if not 100%) should ideally be statically typed at any given moment, right from the start. This does not mean though that you need to manually specify types everywhere - you can rely heavily on type inference.


My view is that if you feel the need to define a type up front, it may be because you're overusing it; it's being referenced by too many different functions. This often indicates poor separation of concerns between components (low cohesion) and tight coupling.

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.


It sure helps to know that something is an object or an array and thus intellisense can actually do something useful for you before you run in the bowser, or what an API returns, or a parameter `users` to a function is an array of users vs a Set of users, or that the the field on a `User` type went from companyName to organizationName.

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.


> the reason why coding is so difficult is that interfaces are often too darn complicated to keep track of?

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()


You're a lot smarter, and have significantly better memory than I do. Once an application has like beyond 10 different interfaces (whther data structures or function signatures) I simple cannot memorise them flawlessly.


Had you laid on the sarcasm a bit less thickly I might be more sympathetic but I'm not, Mr. smartest-person-in-the-room. Keep looking down on all the idiots who think they know more than you and wasted their lives producing blatant rubbish.

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)


> 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

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.


I think that’s a major simplification of what’s going on in software development. Alan Kay invented OO, and everyone says the same thing about OO - that it’s over complicated and slows down development.

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.


Functional programming does this. Probably not Haskell level ninja types or passing functions around like dependency injection, but the idea of separating state and io away from pure logic does indeed remove complexity.

Ironically the separation of state away from logic is why backend web development tends to be easier than say game development.


In my experience it's quite difficult to keep things simple. It's a constant battle and sometimes we fail.

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.


At some point someone needs to have the balls to create and push a better language than JS in the browser. It's a huge and scary undertaking, but until it happens we are firmly in transpile/good language -> WASM land for the foreseeable future.

TS is lauded, but I really think it's just the best of a bad bunch.


There's Dart.

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.


I don't think Dart is meaningfully better than JS (for some vague definition of better)


It provides opt-in type safety. Unlike TS, it's not a language extension.


There's absolutely no reason to push another language in the browser. Just make wasm your compile target. It's assembly for a reason.


Sure, if you're writing a lib...

If you're writing software and you're consuming libs and frameworks, a fragmented ecosystem sounds less than ideal.


LuaJIT?


Part of the problem that Typescript can help solve is that JavaScript can be tricky with undefined, null and accidentally casting numbers and strings to each other. It's really a godsend for very large web apps that have large teams of developers with more than 0% turnover.


ReScript solves this problem with a much much simpler type system and stronger inference at the cost of forcing you to handle nulls and undefined as Options.

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.


"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?"

- Brian Kernighan


So if I work hard to make a program really easy to debug, then I’m making it harder to debug?


I think the term "clever" is used to describe unnecessary complexity and indirection, which makes the code harder to debug.

I agree with the notion that simplicity is hard though.


Easy: I'll invest the same effort but for the double of time.


is your name Criss by any chance?


At the risk of too much self promotion, I have a much deeper and more feature complete treatment of many of these type-level computations, represented with higher kinded types (which are thus composable) here:

http://hkt.code.lol/

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.


I know I'll likely get a biased answer here but what do you think the cons are of using types in this way? My current thinking is that types should should optimize for an easy learning curve so folks can just get on with writing code. This seems like it would require a deep knowledge of TS.

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.


Conversely, and I don't mean to sound flippant, but are there any pros of (ab)using types in this way?

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.


The pros of any type-level logic all boil down to automatically enforcing something that you want to be enforced. The key word being _automatically_.

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.


> provide browsable documentation about the constraint its enforcing and how other places in the code resolve the constraint.

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


That doesn’t seem like a fair characterization in all cases.


It's a fair characterization because it's a common enough issue to become a problem. It's a compounding of several issues, really, of which "types are documentation" is just one:

- types are not documentation

- tests are not documentation

- code is not documentation


What would you call the ability of a type to communicate information such as the range of values that can be passed into a function?


I'd call it exactly that.

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.


Can you look at which fields are required to make a value of type `Generic`, `GenericV2`, etc.?


Generally (at least if we're talking typescript and vscode), the IDE serves as a REPL UI for the type system.

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.


For errors that can be automatically fixed by IDE, yes. It doesn't make this documentation ;)

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.


A dozen lines of nested types are like a dozen lines of source code, and I generally recommend treating them the same if they aren't legible: poke it with a REPL (in this case, try to declare a variable or new type that should work if your understanding of the types are correct) and see what the tools say.

(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.


> A dozen lines of nested types are like a dozen lines of source code, and I generally recommend treating them the same if they aren't legible: poke it with a REPL (in this case, try to declare a variable or new type that should work if your understanding of the types are correct) and see what the tools say.

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 :)


> None of these actions make types documentation.

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).


> By the same argument, code isn't documentation.

It isn't.

> all manner of style guides will tell us

A lie parroted ad infinitum doesn't make it truth.


Can you clarify your belief that code isn't documentation? I'm far more familiar with the alternate philosophy that code is just another way humans communicate with each other (the computer doesn't care; the computer can take a machine-compatible binary from anywhere and execute it with no care as to whether humans understand the how or why of the program). Consider the "Literate programming" approach and tooling that operates in that paradigm (such as Jupyter notebooks).

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.


Documentation: "is any communicable material that is used to describe, explain or instruct regarding some attributes of an object, system or procedure, such as its parts, assembly, installation, maintenance and use."

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.


But projects like these go far beyond just constraints. They're a library of utility functions for doing arithmetic, working with lists, and even sorting. All of this can be equally accomplished by a regular and well-established library that doesn't depend on exotic, and possibly undocumented, features of a type system, and without incurring build time penalties.


Well, "constraint" is a very abstract word. What you're describing are still constraints, they're just complex constraints.

The type vs. no type argument is basically: what's the cutoff line for constraints that are worth encoding as types or not?


If you push all your code into the type system, you're basically just writing your program in a clunkier, less readable language. So, you will just end up having to debug your type system, which defeats the whole purpose. I think expressive type systems are great, but like most things in engineering, it's a trade-off; the complexity of having to manually manage invariants vs. the complexity of managing the type system.


The correct balance depends on how critical the piece of code you are working on is, and how much you want to write unit tests for it.

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.


> If you push all your code into the type system, you're basically just writing your program in a clunkier, less readable language.

It would be funny to see web servers running the typescript compiler (type checker) instead of a JS runtime, though.


Well, slower compile times, and additional complexity are the biggest cons.

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"


Could you elaborate on the project? I'm curious


I love this. I'm frequently experimenting with the "syntactic sugar" possibilities of the TypeScript type inference system, and am truly in awe of your implementation (as well as the excellent and thorough commenting).


Proof that type systems are nothing else that domain-specific languages that run at compile time. When seen like that, it should be no surprise that they can be coerced to do general computation.


We do a lot of this in computing and engineering. Reinventing the same thing under different names. A JIT is nothing but "compiler at runtime". And a compiler, parser, tokenizer are all converting one representation into another. Compile-time and run-time are completely nominal, there's no reason why we should not start reducing terms as we type, with the "run-time" engine (of course the runtime should know what causes side-effects and stop there).

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.


> Eventually we need to make type systems just metaprogramming using the primary language

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 should add that Nim still still has a 'separate language' for types, so doesn't quite fit OP's bill. Nevertheless, it's quite easy to build type constructs using statically resolvable expressions.

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-...


You’ve invented lisp.

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.


I clearly haven't invented LISP, because the limitations you mention are not inherent to what I'm describing. LISP (and SmallTalk, and Erlang) had many correct ideas, but drastically underperformed in others. Unfortunately we threw out the baby with the bathwater.

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 agree with the premise - type-level logic is still just logic, so why not unify the syntax? There's a specification / model checking system called TLA+. This is exactly how you specify types - just as predicates in the same logic as the behavior is defined in. The issue is, it's not statically checkable.

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.


That’s not universally true, and indeed some languages (e.g. Haskell without extensions) have explicitly designed decidable type systems.


That just means that it isn’t Turing complete which is true of many DSLs.


Which means you can't to general computation with it. You're just reiterating his point.


Regex (at least most implementations) isn't Turing complete, but you can still do a lot of stuff with it and I think most type systems go far beyond regex in expressive power. Unless you're trying to write quines, you probably don't need Turing completeness.


> else that domain-specific languages that run at compile time.

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 difference between regular languages and "type system languages" (at least well designed ones) is that the latter are decidable. In that sense another typesystem lang for the typesystem lang would still be a decidable one.

The most generic one I know of is Idris. It uses some heuristics to make some functions decidable that in theory should not be.


Welcome to Lisp.


Some languages have type systems like that, either deliberately as in the case of Idris and Agda or accidentally as in C++ or Typescript.

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.


It wasn't an accident in Typescript. Each step along the way seemed well deliberated and to facilitate reasoning about someone's code, despite the computational complexity trade-offs.

(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.)


Indeed, type systems are domain-specific languages, that run at compile time or runtime, and specify constraints about values.

Most of them can be coerced to do general computation, especially if they have things like if-branch, and some sort of self-reference.


Most of them can't be coerced like this. Typescript is one of the few that can be.


The type systems of C++, C#, Java, Scala, Rust, Haskell, Swift etc all Turing complete/undecidable, aren't they?


It is no surprise. They can be coerced to prove your program correct, negating the need to do unit tests.

But this is only true for certain languages. Overall most type systems are not Turing complete and therefore not suited for general computation


Yes. We have known this since C++ template metaprogramming.


The following homage to Aphyr’s "Typing the technical interview" solves the N-queens problem using types in Typescript. This is both funny and insightful.

https://www.richard-towers.com/2023/03/11/typescripting-the-...

(Edit: this is mentioned elsewhere in this thread by Kerrick)


For those who are wondering whether types can be computed in the same way as values -- yes, they can, with a proper type system. Idris [1] is a prominent example of a programming language with first-class types, meaning that if you can perform (finite) computation on values, you can do so on types. The difference between Idris and TypeScript (Rust, C++, etc.) is that the former treats types as values, and therefore has no traditional separation of values and types; everything is done with the same syntax.

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.

[1] https://www.idris-lang.org/


I recently learned about Typescript transformers[0] and wish they were better supported in a first-class manner. Working with ttypescript or ts-patch[1] is easy enough, but it still feels like you're doing something you shouldn't be doing.

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

[1] https://github.com/nonara/ts-patch


SQL implemented in typescript types - https://github.com/codemix/ts-sql


Is TS becoming the leader in faux dependent typing ? it seems that the metalevel boundary has almost evaporated in it.


Looks like the arithmetic only supports integers 0 through 10

https://github.com/ronami/meta-typing/blob/master/src/utils/...


You can count to an arbitrary number (until the recursion limit) by using tuples of length N (and go back and forth to number literals by using the type of tuple['length'])

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>


A tuple does have a upper limit of 10 000 elements, which means that with this approach we can count to 10 000 at most.

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/...


"This project attempts to push TypeScript's type system to its limits by actually implementing various functions and algorithms, purely on top of the type system... Please note that this project is meant to be used for fun and learning purposes and not for practical use"


It is indeed interesting! I was just trying to analyze it a bit deeper


Noticed the same. Not a very practical computer, although you could say the host CPU does the same except for int's in 0 through 2^64-1.




so a typescript pattern I keep tripping into, which is probably bad practice on my part, is parallel data structures. say any large state object. I have an object the same shape but holding errors of subkeys. the state setter clears the errors object keys per access modification. TS does not like that error object. it can never have an error there isn't a setter for, but it complains it might not.



Yes, of course.

Once we annihilate all human life on earth, the stock market will be at $0.


oh, C++ metaprogramming!?


is<std::this_cpp<meta::pro>, std::enable_if_v<gramming>>>>>>>>>>>>>>?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: