I generally do this via a `throw UnsupportedValueError(value)`, where the exception constructor only accepts a `never`. That way I have both a compile time check as well as an error at runtime, if anything weird happens and there's an unexpected value.
It is meant to be a much better alternative to Javascript while dealing with the fact that the underlying engines use and existing programmers were used to Javascript.
That said I absolutely enjoy TypeScript, but that might be because I suffered from having to deal with Javascript from 2006 until TypeScript became available.
I have the exact same reason I enjoy typescript - raw dogging js before was an absolute nightmare of testing every single function for every possible value that might be thrown in and being able to handle shit data everywhere.
Writing TypeScript is better than JavaScript, but the lack of runtime protection is fairly problematic.
However, there are libraries such as https://zod.dev, and you can adopt patterns for your interfaces and there's already a large community that does this.
Zod is quite unpleasant to use, IME, an has some edge cases where you lose code comments.
From experience, we end up with a mix of both Zod and types and sometimes types that need to be converted to Zod. It's all quite verbose and janky.
I quite like the approach of Typia (uses build-time inline of JavaScript), but it's not compatible with all build chains and questions abound on its viability post Go refactor.
If Typescript is javascript with types bolted on, Rescript is javascript with types the way it should have been. Sound types with low complexity. https://rescript-lang.org/
That syntax is so alien to me as a JS/TS developer. I mean coffeescript was as well until JS slowly introduced it all.
It would be cruel for me to force ReScript onto the team because they'd all need to reskill. I could only use it for a private project and then hire exclusively for it afterwards
Really, that is surprising to hear. There are a couple of differences but most of the syntax looks the same to me, what part do you find alien?
The reskill problem is of similiar difficulty with learning a new framework I think. Especially because the language is rather simple compared to typescript (which is also its strength).
I do understand it is an uphill battle. The whole nobody get's fired for choosing IBM thing. The language is still unproven in the general perception. I do think that when it comes to libraries and frameworks I see a lot of developers choose new unproven stuff, more then they do languages.
Isn't that not necessarily out of the ordinary though? What if there's a cosmic ray that change's the value to something not expected by the exhaustive switch? Or more likely, what if an update to a dynamic library adds another value to that enum (or whatever)? What some languages do is add an implicit default case. It's what Java does, at least: https://openjdk.org/jeps/361
This is how all static type checking works. What programming language do you have in mind that does static type checking and then also does the same type checking at runtime? And what would you expect this programming language to do at runtime if it finds an unexpected type?
I think the point is that other languages make guarantees that ensure you don't have to do any runtime checking. In TypeScript, it's far too easy (and sometimes inevitable) to override the type checker, so some poor function further down into the codebase might get a string when it expects an object, even though there are no type errors.
> The fact that there can be runtime type errors that were proven impossible at compile time is why I will never enjoy TypeScript.
The "impossibility" is just a trait of the type definitions and assertions that developers specify. You don't need to use TypeScript to understand that impossibilities written in by developers can and often are very possible.
My first introduction to TypeScript was trying to use it to solve Advent of Code.
I wrote some code that iterated over lines in a file or something and passed them to a function that took an argument with a numeric type.
I thought this would be a great test to show the benefits of TypeScript over plain JavaScript: either it would fail to compile, or the strings would become numbers.
What actually happened was it compiled perfectly fine, but the "numeric" input to my function contained a string!
I found that to be a gross violation of trust and have never recovered from it.
> I thought this would be a great test to show the benefits of TypeScript over plain JavaScript: either it would fail to compile, or the strings would become numbers.
You're just stressing that you had a fundamental misunderstanding of the language you were using when you first started to use it. This is not a problem with the language. You simply did not understood what you were doing.
For starters, TypeScript does not change the code you write, it only helps you attach type information to the code you write. The type information you add is there to help the IDE flag potential issues with your code. That's it.
See how node introduced support for running TypeScript code: it strips out type info, and runs the resulting JavaScript code. That's it.
If your code was failing to tell you that you were passing strings where you expected numbers, that was a bug in your code. It's a logic error, and a type definition error.
Static code analysis doesn't change the way you write code. That's your responsibility. Static code analysis adds visibility to the problems you're creating for yourself.
No tool is perfect. What matters is if a tool is useful. I've found TypeScript to be incredibly useful. Is it possible to construct code that leads to runtime type errors? Yes. Does it go a long way towards reducing runtime type errors? Also yes.
> No tool is perfect. What matters is if a tool is useful
Some tools are more perfect and more useful than others.
Typescript's type system is very powerful, but without strict compile-time enforcement you still spend a lot of effort on validating runtime weirdness (that the compiler ought to be able to enforce).
> Typescript's type system is very powerful, but without strict compile-time enforcement you still spend a lot of effort on validating runtime weirdness (that the compiler ought to be able to enforce).
That's something that you own and control, though. Just because TypeScript allows developers to gently onboard static type checking by disabling or watering down checks, that does not mean TypeScipt is the reason you spend time validating your own bugs.
> Just because TypeScript allows developers... does not mean TypeScipt is the reason you spend time validating your own bugs
Unfortunately, taking an ecosystem-wide view, it means exactly that. If one of my dependencies hasn't provided type stubs, or has provided stubs, but then violated their own type signatures in some way, I'm on the hook for the outputs not matching the type annotations.
In a strict language, The compiler would assert that the dependency's declared types matched their code, and I'd only be on the hook for type violations in my own code.
Yes that's true, but there's effort to consider on both sides of design decisions like those TypeScript has made. Much of the compile time behaviour comes from the decision for TypeScript to be incremental on top of JavaScript. That allows you to start getting the benefit of TS without the effort of having to rewrite your entire codebase, for example. Having used TS for many years now I feel that the balance it strikes is incredibly productive. Maybe for other folks/projects the tradeoff is different - but for me I would hate going back to plain JS, and there's no alternative available with such tight integration with the rest of the web ecosystem.
Have you seen ReScript? Of course it is not as popular as typescript but it improves on all the bad parts of typescript. You'll get sound types with the pain points of javascript stripped out. Because it compiles to readable javascript you are still in the npm ecosystem.
You don't have to rewrite your whole codebase to start using it. It grows horizontally (you add typed files along the way) compared to typescript which grows vertically (you enable it with Any types).
The point is that we don't have to move back to plain js. We have learned a lot since typescript was created and I think the time has come to slowly move to a better language (and ReScript feels the most like Javascript in that regard).
Both are by design. Array covariance is a common design mistake in OOP languages, which the designer of TypeScript had already done for C# but there they at least check it at runtime. And the latter was declared not-a-bug already IIRC.
TypeScript designers insist they're ok with it being unsound even on the strictest settings. Which I'd be ok with if the remaining type errors were detected at runtime, but they also insist they don't want the type system to add any runtime semantics.
"By design", for me, doesn't say that it can't be changed — maybe the design was wrong, after all. Would it be a major hurdle or create some problems if fixed today?
In the first example you deliberately create an ambiguous type, when you already know that it's not. You told the compiler you know more than it does.
The second is a delegate, that will be triggered at any point during runtime. How can the compiler know what x will be?
First example: you're confusing the annotation for a cast, but it isn't; it won't work the other way around. What you're seeing there is array covariance, an unsound (i.e. broken) subtyping rule for mutable arrays. C# has it too but they've got the decency to check it at runtime.
Second example: that's the point. If the compiler can't prove that x will be initalised before the call it should reject the code until you make it x: number|undefined, to force the closure to handle the undefined case.
For the first one, the compiler should not allow the mutable list to be assigned to a more broadly typed mutable list. This is a compile error in kotlin, for example
val items: MutableList<Int> = mutableListOf(3)
val brokenItems: MutableList<Any> = items
If it only works when you write the types correctly with no mistakes, what's the point? I thought the point of all this strong typing stuff was to detect mistakes.
Because adding types adds constraints across the codebase that detect a broader set of mistakes. It's like saying what's the point of putting seatbelts into a car if they only work when you're wearing them - yes you can use them wrong (perhaps even unknowingly), but the overall benefit is much greater. On balance I find that TypeScript gives me huge benefit.
Same here, you can also use the same function in switch cases in Angular templates for the same purpose. Had no idea you could achieve similar with `satisfies`, cool trick.
I would highly recommend the ts-pattern [1] library if you find yourself wanting exhaustive switch statements! The syntax is a bit noiser than case statements in simple cases, but I find it less awkward for exhaustive pattern matching and much harder to shoot yourself in the foot with. Once you get familiar with it, it can trim down a /lot/ of more complicated logic too.
It also makes match expressions an expression rather than a statement, so it can replace awkward terenaries. And it has no transitive dependencies!
Nice. I didn’t know I can now replace my “assertExhaustive” function.
Previously you could define a function that accepted never and throws. It tells the compiler that you expect the code path to be exhaustive and fixes any return value expected errors. If the type is changed so that it’s no longer exhaustive it will fail to compile and (still better than satisfies) if an invalid value is passed at runtime it will throw.
I thought the same thing. I also have an assert function I pull in everywhere, and this trick seemed like it would be cleaner (especially for one-off scripts to reduce deps).
But unfortunately, using a default clause creates a branching condition that then treats the entire switch block as non-exhaustive, even though it is technically exhaustive over the switch target. It still requires something like throwing an exception, which at that point you might as well do 'const x: never = myFoo'.
class AbsurdError extends Error {
constructor(public value: unknown, message: string) {
super(message);
this.name = 'AbsurdError';
}
}
function absurd(value: never, message: string) {
throw new AbsurdError(value, message);
}
Including an error message and an error type helps if one does slip through to runtime. Additionally, the AbsurdError can be caught and escalated appropriately. And finally the absurd function can be used in an inline ternary etc. where alternatives like throw cannot.
Yeah, the satisfies operator is powerful. One thing it allows you to do is to check that one expression is assignable to a type, without actually declaring or coercing the expression to be of that type.
In my experience, here is summary of when you might want to use the various type features:
- Using the "as" keyword tries to coerce/cast an expression to be of some type. This is the least safe, since it overrides proper type checking, at least to some extent. However, in many cases it still does some level of type checking, and it is sometimes necessary if you know something the type checker is unable to prove.
- Declaring an expression to be of some type before initializing it. This is the strictest, and thus most safe, but sometimes it is inconvenient. You might want types to be inferred, which is a really useful thing sometimes, e.g., in a fluent interface when each step constrains types in additional ways.
- The satisfies keyword bridges the gap between the above two: you want to assert/check that the expression is assignable to a certain type (which can be as loose or strict as you want it), whilst still retaining the original type that is inferred for the expression. Unlike "as", you are not overriding the type system. And you are also not actually declaring a type for the expression. I have found this very useful on numerous occasions.
Having said that, there is weird (new?) behavior of the satisfies keyword where appending it to an expressions can cause the the expression itself to fail type-checking internally, where it would otherwise be fine.
This is strange, the basic premise of satisfies is to check if an expression satisfies a type. If there is a type violation at all, it should be at the expression/type boundary, not inside the expression.
> TypeScript is a wonderfully advanced language though it has an unfortunately steep learning curve
An extremely steep one.
The average multi-year TypeScript developer I meet can barely write a basic utility type, let alone has any general (non TypeScript related) notion of cardinality or sub typing. Hell, ask someone to write a signature for array flat, you'd be surprised how many would fail.
Too many really stop at the very basics.
And even though I consider myself okay at TypeScript, the gap with the more skilled of my colleagues is still impressively huge.
I think there's a dual problem, on one side type-level programming isn't taken seriously by the average dev, and is generally not nurtured.
On the other hand, the amount of ideas, theory, and even worse implementation details of the TypeScript compiler are far from negligible.
Oh, and it really doesn't help that TypeScript is insanely verbose, this can easily balloon when your signatures have multiple type dependencies (think composing functions that can have different outputs and different failures).
is far from basic Typescript. The average Typescript dev likely doesn't need to understand recursive conditional types. It's a level of typescript one typically only needs for library development.
Not only have I never been expected to write something like this for actual work, I'm not sure it's been useful when I have, since most of my colleagues consider something like this nerd sniping and avoid touching/using such utilities, even with documentation.
If I saw that in a PR I would push very hard to reject; something like that is a maintenance burden that probably isn’t worth the cost, and I’ve been the most hardcore about types and TypeScript of anyone of any team I’ve been on in the past decade or so.
Now, that said, I probably would want to be friends with that dev. Unless they had an AI generate it, in which case the sin is doubled.
I think there’s a difference between what’s expected/acceptable for library code vs application code. Types like this might be hard to understand, but they create very pleasant APIs for library consumers. I’ve generally found it very rare that I’ve felt the need to reach for more complex types like this in application code, however.
RXJS’s pipe function has a pretty complex type for its signature, but as a user of the library it ‘just works’ in exactly the type-safe way I’d expect, without me having to understand the complexity of the type.
> If I saw that in a PR I would push very hard to reject; something like that is a maintenance burden that probably isn’t worth the cost
As someone who came from a CS background, this kind of attitude is deeply mysterious. That seems like a type expression I'd expect a CS undergrad to be able to write - certainly if an SDE with 1-2 years experience was confused by it, I'd be advocating against their further promotion.
The every day practice of software engineering has little to do with the academic discipline of computer science. What makes a good software engineer is not usually the same thing that makes a good CS major
Sure, but basic CS knowledge is an expectation in much of the software field (albeit less since the mid-2010's javascript boom). A lot of companies aren't going to hire you if you don't know the basics of data structures and algorithms
but then you wind up with an entire repo, or an entire engineering team utterly hobbled by a lack of expressive typing (or advanced concepts generally) and debased by the inelegance of basic bitch programming.
If it's correct, it's not a maintenance nightmare, and it will alert you to problems later when someone wants to use it incorrectly.
If you're writing first-party software, it probably doesn't matter. But if you have consumers, it's important. The compiler will tell you what's wrong all downstream from there unless someone explicitly works around it. That's the one you want to reject.
> If it's correct, it's not a maintenance nightmare, and it will alert you to problems later when someone wants to use it incorrectly.
You're confusing things. It is a maintenance nightmare because it is your job to ensure it is correct and remains correct in spite of changes. You are the one owning that mess and held accountable for it.
> If you're writing first-party software, it probably doesn't matter. But if you have consumers, it's important.
Yes, it is important that you write correct and usable code. That code doesn't fall on your lap though and you need to be the one writing and maintaining it. Whoever feels compelled to write unintelligible character soup that makes even experienced seasoned devs pause and focus is failing their job as a software engineer.
> Whoever feels compelled to write unintelligible character soup...
I see it differently. That's the name of the game. Language design is always striving toward making it more intelligible, but it is reasonable to expect pros to have command of the language.
> I see it differently. That's the name of the game. Language design is always striving toward making it more intelligible, but it is reasonable to expect pros to have command of the language.
No, that's an extremely naive and clueless opinion to have. Any basic book on software engineering will tell you in many, many ways that the goal of any software engineer is to write simple code that is trivial to parse, understand, and maintain, and writing arcane and overly complex code is the Hallmark of an incompetent developer. The goal of a software engineer is to continuously fight complexity and keep things as simple as they can be. Just because someone can write cryptic, unintelligible code that doesn't make them smart or clever: it only makes them bad at their job.
I’d say it depends. I always advocate for code that is easy to read and to understand, but in extremely rare conditions, hard to read code is the better solution.
Especially when it comes to signatures in Typescript, complex signatures can be used to create simple and ergonomic APIs.
But anyway you shouldn’t be allowed to push anything like this without multiple lines of comments documenting the thing. Unreadable code can be balanced with good documentation but I rarely saw this unfortunately.
looking back at them is also real hard to debug. you dont get a particularly nice error message, and a comment or a test would tell better than the type what the thing should be looking like
The alternative is what shows in the comment: go on HN and tell the world you think TS and JS are crap and it's not worth your time, while writing poor software.
To answer this we probably need more details, otherwise it's gonna be an XY Problem. What is it that I'm trying to do? How would I type this function in, say, SML, which isn't going to allow incorrect types but also doesn't allow these kinds of type gymnastics?
We don't have to deal in hypotheticals - we have a concrete example here. There's a method, array.flat() that does a thing that we can correctly describe in TypeScript's type system.
You say you would reject those correct types, but for what alternative?
It's hugely beneficial to library users to automatically get correctly type return values from functions without having to do error-prone casts. I would always take on the burden of correct types on the library side to improve the dev experience and reduce the risk of bugs on the library-consumption side.
There's nothing I can do about the standard JavaScript library, but in terms of code I have influence over, I very simply would not write a difficult-to-type method like Array.prototype.flat(), if I could help it. That's what I mean by an XY Problem - why are we writing this difficult-to-type method in the first place and what can we do instead?
Let's suppose Array.prototype.flat() wasn't in the standard library, which is why I'm reviewing a PR with this gnarly type in it. If I went and asked you why you needed this, I guess you'd say the answer is: "because JavaScript lets me make heterogenous arrays, which lets me freely intermix elements and arrays and arrays of arrays and... in my arrays, and I'm doing that for something tree-like but also need to get an array of each element in the structure". To which I'd say something like "stop doing that, this isn't Lisp, define an actual data type for these things". Suddenly this typing problem goes away, because the type of your "flatten" method is just "MyStructure -> [MyElements]".
Sure, if you're living fully in your own application code, and you don't need to consume things from an API you don't control, it's easy to live in a walled garden of type purity.
I can recognize that most people are going to go for inaccurate types when fancier semantics are necessary to consume things from the network.
But we also have the real world where libraries are used by both JS devs and TS devs, and if we want to offer semantics that idiomatic for JS users (such as Array.prototype.flat()) while also providing a first-class experience to TS consumers, it is often valuable to have this higher-level aptitude with the TS type system.
As mentioned earlier, I believe 90% of TS devs are never in this position, or it's infrequent enough that they're not motivated to learn higher-level type mechanics. But I also disagree with the suggestion that such types should be avoided because you can always refactor your interface to provide structure that allows you to avoid them; You don't always control the shape of objects which permeate software boundaries, and when providing library-level code, the developer experience of the consumer is often prioritized, which often means providing a more flexible API that can only be properly typed with more complex types.
> Suddenly this typing problem goes away, because the type of your "flatten" method is just "MyStructure -> [MyElements]".
How is that less maintenance burden than a simple Flatten type? Now you have to construct and likely unwrap the types as needed.
And how will you ensure that you're flattening your unneeded type anyways? Sure you can remove the generics for a concrete type but that won't simplify the type.
It's simple. It's just recursive flattening an array in 4 lines. Unlikely to ever change, unlike the 638255 types that you'd have to introduce and maintain for no reason.
There are many reasons not to do that. Say your business logic changes and your type no longer needs one of the alternatives: you are unlikely to notice because it will typecheck even if never constructed and you will have to deal with that unused code path until you realize it's unused (if you ever do).
You made code harder to maintain and more complex for some misguided sense of simplicity.
Right, from the structure you get an array with one element which is likely an union type from that naming.
Honestly, you sound more like your arguing from the perspective of a person unwilling to learn new things, considering you couldn't even get that type correct.
To begin with, that flat signature wasn't even hard to understand?
What I wrote would be a syntax error in TypeScript (no name for the argument, wrong arrow), not a function that returns array with one element; I used Haskell-ish notation instead of TypeScript's more verbose "(structure: MyStructure) => MyElement[]".
I thought it was clear enough that I was being informal and what I meant was clear, but that was admittedly probably a mistake. But to infer an implication from that that I'm "unwilling to learn new things" is a non sequitur and honestly kind of an unnecessarily dickish accusation.
Brah, If you have a type with that many characters in it that isn’t a super long string name, it’s not easy to understand unless you are the 1% of 1% when it comes to interpreting this specific language.
On top of that I fully agree with the poster you’re responding to. In general application code that’s and extremely complicated type, generally done by someone being as clever as can be. And if the code you’ve written when you’re being as clever as possible has a bug in it, you won’t be clever enough to debug it.
For one, the simple answer is incomplete. It gives the fully unwrapped type of the array but you still need something like
type FlatArray<T extends unknown[]> = Flatten<T[number]>[]
The main difference is that the first, rest logic in the complex version lets you maintain information TypeScript has about the length/positional types of the array. After flattening a 3-tuple of a number, boolean, and string array TypeScript can remember that the first index is a number, the second index is a boolean, and the remaining indices are strings. The second version of the type will give each index the type number | boolean | string.
The answer above actually gets the type union of all non-array elements of a multi-level array.
In other words
Flatten<[1,[2,'a',['b']]]>
will give you a union type of 1, 2, 'a', and 'b'
const foo: Flatten<[1,[2,'a',['b']]]> = 'b'; // OK
const bar: Flatten<[1,[2,'a',['b']]]> = 'c'; // Error: Type '"c"' is not assignable to type '1 | 2 | "a" | "b"'
Technically the inference is unnecessary there, if that's you're goal:
type Flatten<T> = T extends Array<unknown> ? Flatten<T[number]> : T
I don't really consider this the type of flattening an array, but `Array<Flatten<ArrType>>` would be. And this would actually be comparable to the builtin Array.prototype.flat type signature with infinite depth (you can see the typedef for that here[1], but this is the highest level of typescript sorcery)
My solution was for flattening an array with a depth of 1 (most people using Array.prototype.flat are using this default depth I'd wager):
Here’s the fun part that I suspect many here are forgetting: if you want to write the function body, it will probably (or at the very least can) look very similar!
function flat() {
if (this.length > 0) {
let [first, ...rest] = this;
if (Array.isArray(first)) {
return [...first, ...flat(rest)];
} else {
return [first, ...flat(rest)];
}
} else {
return [];
}
}
I still wouldn’t call it basic TypeScript, but it’s not conceptually that advanced, you just need to know about infer and extends.
Now in reality, Array.prototype.flat has a more complex definition, partly because (like most of Array’s methods) the method is generic (it works on array-like objects that have a length property and numeric indexing), and partly because of the depth parameter. From lib.es2019.array.d.ts:
Ouch. Don’t like the { done, recur }[Depth extends -1 ? "done" : "recur"] at all, no idea why it wasn’t written as `Depth extends -1 ? Arr : Arr extends ReadonlyArray<…`. And as for hard-coding support for depths up to 20 then bailing… probably pragmatic, it’s possible to support all values, but rather messy: https://stackoverflow.com/q/54243431.
You're missing the specialisation of Object/Any. For example Array.flat called with [int, [bool, string]] returns a type [int, bool, string]. Admittedly this is somewhat niche, but most other languages can't express this - the type information gets erased.
You're missing the input type, essentially. Those are just array types. The TypeScript type signature more of a function type, it expresses flattening a n-dimensional array (input type) into a flat array (output type).
I don’t think that means it has a steep learning curve. It just means the basics suffice for a ton of TypeScript deployments. Which I personally don’t see as the end of the world.
Yes, to me this is a biggest feature of Typescript: A little goes a long way, while the advanced features make really cool things possible. I tend to think of there being two kinds of Typescript - Application Typescript (aka The Basics, `type`, `interface`, `Record`, unions etc...) and Library Typescript which is the stuff that eg Zod or Prisma does to give the Application Typescript users awesome features.
While I aspire to Library TS levels of skill, I am really only a bit past App TS myself.
On that note I've been meaning to the the Type-Level Typescript course [0]. Has anyone taken it?
As someone who knows slightly more than the basics, and enough to know about the advanced stuff that I don't know about, this is the correct place to stop.
I would much rather restructure my javascript than do typescript gymnastics to fit it into the type system.
It's also terribly documented. As an example, I don't think `satisfies` is in the docs outside of release notes. There's lots more stuff like that, which makes using it kind of frustrating.
Being a JS/TS one-trick pony all my career, how does it compare to other languages? I don't really see much difference, except if comparing with some C++ shenanigans.
> "There basics," well understood and judiciously applied, is where the bulk of TypeScript's value lies.
Yes, precisely. OP is also completely oblivious to the fact that TypeScript is designed to help developers gradually onboard legacy JavaScript projects and components, which definitely don't require arcane and convoluted type definitions to add value.
these are things most developers don't know how to do in most language's type systems. I think only rust with its focus on functional roots has seen similar focus on utilizing its type system to its fullest extent.
TypeScript codebases I've seen generally seem to have the widest demonstration of skill gap versus other languages I use.
For example, I don't ever see anyone using `dynamic` or `object` in C#, but I will often see less skilled developers using `any` and `// @ts-ignore` in TypeScript at every possible opportunity even if it's making their development experience categorically worse.
For these developers, the `type` keyword is totally unknown. They don't know how to make a type, or what `Omit` is, or how to extend a type. Hell, they usually don't even know what a union is. Or generics.
I sometimes think that in trying to just be a superset of JavaScript, and it being constantly advertised as so, TypeScript does not/did not get taken seriously enough as a standalone language because it's far too simple to just slot sloppy JavaScript into TypeScript. TypeScript seems a lot better now of having a more sane tsconfig.json, but it still isn't strict enough by default.
This is a strong contrast with other languages that compile to JavaScript, like https://rescript-lang.org/ which has an example of pattern matching right there on the home page.
Which brings me onto another aspect I don't really like about TypeScript; it's constantly own-goaling itself because of it's "we don't add anything except syntax and types" philosophy. I don't think TypeScript will ever get pattern matching as a result, which is absurd, because it has unions.
> For example, I don't ever see anyone using `dynamic` or `object` in C#, but I will often see less skilled developers using `any` and `// @ts-ignore` in TypeScript at every possible opportunity even if it's making their development experience categorically worse.
I think you're confusing things that aren't even comparable. The primary reason TypeScript developers use the likes of `any` is because a) TypeScript focuses on adding support for static type checking on a language that does not support it instead of actually defining the underlying types, b) TypeScript developers mostly focus on onboarding and integrating TypeScript onto projects and components that don't support it, b) TypeScript developers are paid to deliver working projects, not vague and arbitrary type correctness goals. Hence TypeScript developers tend to use `any` in third party components, add user-defined type guards to introduce typing in critical areas, and iterate over type definitions when time allows.
Discriminating a function or promise based on return type is never going to work, because JavaScript is dynamically typed and TypeScript erases types at compile time, so there's no way to know at runtime what type a function or promise is going to return.
> The average multi-year TypeScript developer I meet can barely write a basic utility type, let alone has any general (non TypeScript related) notion of cardinality or sub typing. Hell, ask someone to write a signature for array flat, you'd be surprised how many would fail.
I think you're both exaggerating your blanket accusations of incompetence and confusing learning curve with mastering extremely niche techniques akin to language gotchas.
to our defense, we want to build stuff not become ts wizards. also I've worked with libraries with heavy heavy typing that it was a nightmare if you wanted to use their lib in any other way than what they have imagined.
You're right, but that begs the question: does a type system really require such complexity?
I'm aware that type theory is a field in and of itself, with a lot of history and breadth, but do developers really need deep levels of type flexibility for a language to be useful and for the compiler to be helpful?
I think TypeScript encourages "overtyping" to the detriment of legibility and comprehension, even though it is technically gradually typed. Because it is so advanced and Turing complete itself, a lot of brain cycles and discussion is spent on implementing and understanding type definitions. And you're definitely right that it being verbose also doesn't help.
So it's always a bittersweet experience using it. On one hand it's great that we have mostly moved on from dynamically typed JavaScript, but on the other, I wish we had settled on a saner preprocessor / compiler / type system.
The idea is to make libraries preserve as much type information as possible, as a principle. Once type information is erased it can't be restored. For regular application code you don't need to use those features.
But regular application code also contains libraries. Type information is useful even if you're the only user of those APIs.
My point was more related to the level of expressiveness required of a type system in order to allow a programmer to produce reliable code without getting in their way. I think TypeScript leans more towards cumbersome than useful.
For example, I'm more familiar with Go's type system, which is on the other side of that scale. It is certainly much less expressive and powerful than TypeScript, and I have found it frustrating and limiting in many ways, but in most day-to-day scenarios it's reasonably adequate. Are Go programs inherently worse off than TypeScript programs? Does a Go programmer have a worse experience overall? I would say: no.
In an interview I'd be happy just seeing some reasoning.
IRL I'd be happy with someone at least searching for a definition and trying to learn from it.
I've asked this question multiple times as implementing array flatten used to be our go to ice breaker question, and many devs had no issues reasoning and finding an okay type definition.
Honestly I just use TypeScript to prevent `1 + [] == "1"` and check that functions are called with arguments. I don't care about type theory at all and the whole thing strikes me as programmers larping (poorly) as mathematicians.
then you're creating a giant mess of a soup where the state of your program could have a result, be loading and an error at the same time. If you could recognise that the state of your program is a sum of possible states (loading | success | error), and not their product as the type above you could highly simplify your code, add more invariants and reduce the number of bugs.
And that is a very simple and basic example, you can go *much* further, as in encoding that some type isn't merely a number through branded types, but a special type of number, be it a positive number between 2 and 200 or, being $ or celsius and avoiding again and entire class of bugs by treating everybody just as an integer or float.
For a function setVelocity() that can accept 1..<200. You call it with numbers that you enter directly and types tell you something that would otherwise be a comment on the function, or you do runtime checks elsewhere, and the type becomes proof that you checked them before handing it into the function.
Btw, using “autism” to mean “pedantry” leaves a bit of a bad taste in my mouth. Maybe you could reconsider using it that way in the future.
Pushing everything to types like this creates a different burden where you're casting between types all over the place just to use the same underlying data. You could just clamp velocity to 200 in the callee and save all that hassle.
> Pushing everything to types like this creates a different burden where you're casting between types all over the place just to use the same underlying data.
TypeScript does not perform any kind of casting at all. What TypeScript supports is structural typing, which boils down to allowing developers to specify type hints in a way that allows the TypeScript compiler to determine which properties or invariants are met in specific code paths.
Literal types address a very common and very mundane use case: assert what can and cannot be done with an object depending on what value one of it's fields have.
Take for example authorization headers. When they are set, their prefix tells you which authorization scheme is being used by clients. With typescript you can express those strings as a prefix constrained string type, and use them to have the TypeScript compiler prevent you from accidentally pass bearer tokens to the function that handles basic authentication.
Literal types shine when you are using them to specify discriminant fields in different types. Say for example you have a JSON object that has a `version` field. With literal types you can define different types discriminated by what string value features in it's `version` field, and based on that alone you can implement fully type-safe code paths.
Casting? Not really - i think you’d only need a couple type checks.
Imo this is mostly useful for situations where you want to handle input validation (and errors) in the UI code and this function lives far away from ui code.
Your point about clamping makes sense, and it’s probably worth doing that anyway, but without it being encoded in the type you have to communicate how the function is intended to be used some other way.
Ah, yeah you’re right. I somehow thought typescript could do type narrowing based on checks - like say:
If (i >= 1) {
// i’s type now includes >= 1
}
But that is not the case, so you’d need a single cast to make it work (from number to ClampedNumber<1,200>) or however exactly you’d want to express this.
Tbf having looked more closely into how typescript handles number range types, I don’t think I would ever use them. Not very expressive or clear. I think I hallucinated something closer to what is in this proposal: https://github.com/microsoft/TypeScript/issues/43505
I still think that the general idea of communicating what acceptable input is via the type system is a good one. But the specifics of doing that with numbers isn’t great in typescript yet.
How would you implement it in other languages that support it better? Can you literally just do a range check and the compiler infers its range for types? If so thats actually pretty neat.
I have mixed feelings about Typescript, I hate reading code with heavy TS annotations because JS formatters are designed to keep line widths short, so you end up with a confusing mess of line breaks. Pure JS is also just more readable.
Also you can so easily go overboard with TS and design all sorts of crazy types and abstractions based on those types that become a net negative in your codebase.
However it does feel really damn nice to have it catch errors and give you great autocomplete and refactoring tooling.
> I have mixed feelings about Typescript, I hate reading code with heavy TS annotations because JS formatters are designed to keep line widths short, so you end up with a confusing mess of line breaks. Pure JS is also just more readable.
That's not a TypeScript issue, it's a code quality issue and a skill issue. Anyone can put together an unintelligible mess in any language.
typescript is largely a result of solving a non-existent problem. Yeah JS is finicky & has foot-guns, however they're ways around those foot guns that don't involve typescript.
Rich Hickey in 10 Years of Clojure & Maybe Not then the Value of Values - lays this out - though not meant at typescript but static types in general.
the thing most people don't have proper Javascript fundamentals.
Function signatures: JSDoc works
Most types - use Maps | Arrays
if a value doesn't exist in a map we can ignore it. There's also the safe navigation operator.
Instead of mutable objects - there's ways around this too. Negating types again.
Yeah, I believe that doesn't quite work correctly for nullable fields or cases where the Zod type would be a subtype of the declared type. But it's a really useful technique, because it's a lot easier to work with types you've declared in TypeScript than the ones Zod generates. I'm sure there's scope for a validation library that is designed around the user providing a TypeScript type and then producing an error if the validation doesn't match that type.
Satisfies is very useful as a library author for testing your types. You can write `typetest` files that don't become part of the bundle but are compiled.
One example is that we have a TS library for a JSON-RPC server that is horribly complex on the server side, returning different shapes of output based on input params. I don't think it can be typed with OpenAPI etc, but it can be with Typescript method overloads.
But then instead of writing unit tests that either coerce or mock the server into returning each shape, we can write typetests like this to make sure our type juggling provides the exact right types to developers:
const rpc = null as unknown as Rpc<SomeApi>
const result = await rpc.someApi(input).send()
result satisfies {
value: {
nestedFieldForThisInput: number
}
}
```
The tradeoff is slightly longer compile times for the library since all this is checked at build time, but it provides a lot of flexibility for testing overloads and utility types in isolation.
You can use multiple tsconfig files to remove the compile time issue; this is quite common for test files anyway. Its not unusual to see one for CI & IDE and one for compilation, the compilation one having a much more limited scope of files.
The author gets into that. `Thetype` might be complex. It also protects you from overgeneralizing, like casting to and from `unknown` to escape the type checker.
type Current = {
kind: "ac" | "dc";
amps: number;
}
type Dc = {
kind: "dc";
amps: number;
}
const ac: Current = {
kind: "ac",
amps: 10000000,
}
const handleDc = (thing: Dc) => {}
const badConvert = (c: Current) => ({...c, kind: "dc"});
/**
* Argument of type '{ kind: string; amps: number; }' is not assignable to parameter of type 'Dc'.
Types of property 'kind' are incompatible.
Type 'string' is not assignable to type '"dc"'.(2345)
*/
handleDc(badConvert(ac));
const goodConvert = (c: Current) => ({
...c, kind: "dc",
} satisfies Dc);
handleDc(goodConvert(ac));
/**
* Object literal may only specify known properties, and 'bar' does not exist in type 'Dc'.
*/
const badConvert2 = (c: Current) => ({
...c, kind: "dc", bar: "qwerty"
} satisfies Dc);
The bad convert is actually wrong. It should be refactored to an equality check and throw an error if account kind is not "dc". The compiler is correct and its not a good idea to work around this issue.
No, the point of the function is to convert the type. It doesn't need to check anything, it just forcibly converts any argument to DC.
The compiler isn't complaining because the conversion isn't valid, it's complaining because it doesn't know that the string "dc" should be narrowed down to a literal type, and so it's kept it as broad as possible. Using `satisfies` lets it understand that it needs to do narrowing here.
In fairness I don't think this is the best case for `satisfies`, and some return type annotations would probably work a lot better here, and be clearer to read.
Absolutely. It's contrived example. If you control the API, you'd be better to rewrite it:
type Current = { amps: number; };
type Ac = { kind: "ac" } & Current;
type Dc = { kind: "dc" } & current;
const currentToDc = (c: Current): Dc => ...
const acToDc = (c: Ac): Dc => ...
And it would flow more naturally. Or define a CurrentKind and include it in current if you don't need to handle the scalar. There are plenty of better ways. That's also something that you can become accustomed to and recognize when you have a weak API.
I'm not really trying to make a case for `satifies`, but it can be handy and clean in some circumstances, especially when you're constrained by a third-party API.
Exactly. It's a safer assertion, as the author points out. You can work without it, but it's a little more cumbersome and involves creating the otherwise unnecessary, transient intermediate types.
Because what you wrote broadens the type. `satisfies` validates the type without broadening it.
const c: string = 'c';
This will be of type string instead of type 'c'. This is a barebones example and it already breaks support for template literal types. Imagine how much better `satisfies` is for complex union types.
This is wordier than just "as const", what advantage does it give? (I am a newbie and genuinely don't know)
edit: perhaps the advantage only comes into play for mutable values, where you want a narrower type than default, but not that narrow. Indeed, this is covered in the article, but CTRL+F "as const" doesn't work on the page for whatever reason, so I missed it.
The satisfies keyword is quite different than "as const." What it does is:
1. Enforce that a value adheres to a specific type
2. But, doesn't cause the value to be cast to that type.
For example, if you have a Rect type like:
type Rect = { w: number, h: number }
You might want to enforce that some value satisfies Rect properties... But also allow it to have others. For example:
const a = { x: 0, y: 0, w: 5, h: 5 };
If you wrote it as:
const a: Rect = // ...
TypeScript wouldn't allow you to also give it x and y properties. And if you did:
as Rect
at the end of the line, TypeScript would allow the x, y properties, but would immediately lose track of them and not allow you to use them later, because you cast it to the Rect type which lacks those properties. You could write an extra utility type:
type Location = { x: number, y: number };
const a: Location & Rect = // ...
But that can get quite verbose as you add more fields. And besides: in this example, all we actually are trying to enforce is that the object is a Rect — why do we also have to enforce other things at the same time? Usually TS allows type inference for fields, but here, as soon as you start trying to enforce one kind of shape, suddenly type inference breaks for every other field.
The satisfies keyword does what you want in this case: it enforces the object conforms to the type, without casting it to the type.
const a = { x: 0, y: 0, w: 5, h: 5 } satisfies Rect;
// a.x works
This was a fantastic writeup, thanks. If you don't mind an additional question...
How does this work,
function coolPeopleOnly(person: Person & { isCool: true }) {
// only cool people can enter here
}
const person = {
name: "Jerred",
isCool: true,
} satisfies Person;
coolPeopleOnly(person);
Since
- person isn't const, so person.isCool could be mutated
- coolPeopleOnly requires that it's input mean not only Person, but isCool = true.
If you ignore the `satisfies` for a moment, the type of `person` is the literal object type that you've written (so in this case, { "person": string, isCool: true }). So coolPeopleOnly(person) works, regardless of whether `satisfies` is there, because TypeScript sees an object literal that has all the person attributes and also `isCool: true`.
(You could mutate it to `isCool: false` later, but then TypeScript would complain because `isCool: false` is different to `isCool: true`. When that happens isn't always obvious, TypeScript uses a bunch of heuristics to decide when to narrow a type down to the literal value (e.g. `true` or `"Jerred"`), vs when to keep it as the more general type (e.g. `boolean` or `string`).)
What `satisfies` is doing here is adding an extra note to the compiler that says "don't change the type of `person` at all, keep it how it is, _but_ also raise an error if that type doesn't match this other type".
(This is only partially true, I believe `satisfies` does affect the heuristics I mentioned above, in that Typescript treats it a little bit like `as const` and narrows types down to their smallest value. But I forget the details of exactly how that works.)
So the `coolPeopleOnly` check will pass because the `person` literal has all the right attributes, but also we'll get an error on the literal itself if we forget an attribute that's necessary for the `Person` type.
It does; the code will still type-check without the satisfies operator. satisfies lets you say "if this value doesn't conform to this type then I want that to be an immediate compile error, even if it would otherwise be okay". Which isn't needed all that often since usually getting the type wrong would produce a compile error elsewhere, but occasionally it proves useful. When designing the feature they collected some use cases: https://github.com/microsoft/TypeScript/issues/47920
I've really only found benefit on the return type of functions, when you can say that a type parameter satisfies a type (with the return type being a boolean). This let's you use `if (isPerson(foo))` and typescript will narrow the type appropriately in the conditional
Does anyone know what was used to render these code blocks in the article? The mouseover tooltip is extremely cool. I've never seen anything like it before.
EDIT: I dug through the codebase and determined that it's using Shiki and TwoSlash for the syntax highlighting and tooltips.
> TypeScript is a wonderfully advanced language though it has an unfortunately steep learning curve; in many ways it’s the complete opposite of Go.
Replace "TypeScript" with "C++" and the same can be said.
It is one of the worst languages ever designed and already built on top of a sloppy foundation (Javascript) compared to Go.
The language encourages escape hatches and tons of flexibility on how it checks its types and creates the risk of inconsistency to engineers on which rules to adopt and there is always one engineer that will disagree with some settings and argue to turn on/off a rule to defeat the purpose of the language.
At this stage, its no better than C++ but significantly slower, and I've seen the same mistakes (enums, allowing "as XYZ" casting, etc) in C++ creeping into TypeScript.
Even the entire language parser and type checker is being rewritten in Go. [0]
I disagree. The speed with which Typescript replaced JS as the default is testament to its strength. I remember back is the good old JS only days you had to assume that all the form values we passed and read were strings and not an integer account number for example. Typescript solves these challenges and that alone eliminates a large class of frankly nonsensical bugs.
The reason it is being rewritten in Go is so the compiler can be run at native speeds which is a good thing. Otherwise the only runtime we have available is NodeJS which has its fair share of problems. You can go far with TS with just interface definitions and some generics peppered in for common utilities. The places where all the type gymnastics is necessary are cases I frankly havent encountered in the usual course of development.
Misses the point. Both these languages have their place. Besides there's no benchmarks beyond developers personal opinions that tells us whether a language is browser ready or not.
It prevents you from mutating via the reference that you obtain from `satisfies` without casting its type, yes (or rather more precisely, you can mutate it, but only to the one allowed value).
However, the object can still be mutated via other references to it. TypeScript is full of holes like this in the type system - the problem is that they are trying to bolt types and immutability onto a hot mess that is JS data model while preserving backwards compatibility.
I’m so frustrated by satisfies because it eliminates optional properties.
I want an object of ‘LayerConfig’ elements where each key is the name of a possible layer. Without ‘satisfies’ I have to name every layer twice in my config. But with it, I can’t have optional properties (eg. Half the layers are fine with the default values for some properties).
The best I’ve found is a hack that uses a function. But this whole thing where my key literals widen into “string” is a constant annoyance to otherwise very elegant code.
> Why is the name of person1 of type string and not the literal "Jerred"? Because the object could be mutated to contain any other string.
Not really, if you declare {name: "Jerred" as const}, it's still mutable. Typescript just decided that given certain primitive-like types like strings, it's preferrable to infer string rather than as constant.
Satisfies offers the opposite AS A MOSTLY ORTHOGONAL design decision. It's a happy byproduct that the type inference's behavior is changed.
And this is relevant because it affects technically important situations like deeply nested values NOT being narrowed, but it's also just not a good mental model for what it's supposed to do.
People should assume that given a type literal, that it just infers the widest typing. Incidental behavior that arises from using 'as const', or 'satisfies' should follow it's semantic purpose. If you want specific typing, just build the type - don't use hacks.
Satisfies is useful because sometimes you have something with some typing (often as const for something like utils), that you also need to make sure satisfies some other typing - almost as a constraint.
Would not surprise me if ts team released a keyword that did type inference with narrowest (like as const, but without the readonly).
When you are building even moderately large non-trivial real world software with JS, you would thank your stars that TS exists and that it has features like this.
It's amazing how quickly the basic concepts of software engineering become inadequate when they run up against the real world.
For example, a naive engineer might think, ah, databases have this oFFSET clause, good, I'll use, unaware that is a foot gun for real world performance.
Or they may think the DELETE operation on DBs is a normal thing, unaware that in most cases it should NOT be used at all.
Or they might think loops are idiomatic, unaware that when you are writing large software you should and can probably almost eliminate loops (unless you are using a DSL like SQL extensions, config languages, etc)
Or they may be unaware of the thorny issues around queues (or even why they might be necessary in the first place) and concurrent access to data.
Or they might not understand why the dogma of separating content and presentation is nonsensical in many situations.
I think this is spot on. TS goes very very far without the gymnastics. Its good to know these things, but not even close to necessary to be productive. Not to mention IDE support for TS is on a very different level conpared to the shitshow that is Javascript.
80% of the value of TypeScript is that it will tell you when when you changed or added a parameter and forgot to update it everywhere, you doofus. The other 20% is that it keeps coding agents from going too far off the rails. Trying to use the type system as a metaprogramming language is only valuable as a fun exercise, but of negative value in real world projects.
That's a little ungenerous. Richer types are not intrinsically about metaprogramming, they just let you model your domain more accurately so you can turn runtime errors into build-time errors. If your system already has natural constraints, you get to document them in a machine-checkable way.
99% of my use of `satisfies` is to type-check exhaustivity in `switch` statements:
I generally do this via a `throw UnsupportedValueError(value)`, where the exception constructor only accepts a `never`. That way I have both a compile time check as well as an error at runtime, if anything weird happens and there's an unexpected value.
The fact that there can be runtime type errors that were proven impossible at compile time is why I will never enjoy TypeScript.
TypeScript isn't primarily meant to be enjoyed.
It is meant to be a much better alternative to Javascript while dealing with the fact that the underlying engines use and existing programmers were used to Javascript.
That said I absolutely enjoy TypeScript, but that might be because I suffered from having to deal with Javascript from 2006 until TypeScript became available.
I have the exact same reason I enjoy typescript - raw dogging js before was an absolute nightmare of testing every single function for every possible value that might be thrown in and being able to handle shit data everywhere.
god-awful code.
As a C# dev, backend typescript is fantastic and the type system is light years ahead of C# in expressivity.
But the learning curve... no shit.
Agree wholeheartedly.
Writing TypeScript is better than JavaScript, but the lack of runtime protection is fairly problematic.
However, there are libraries such as https://zod.dev, and you can adopt patterns for your interfaces and there's already a large community that does this.
Zod is quite unpleasant to use, IME, an has some edge cases where you lose code comments.
From experience, we end up with a mix of both Zod and types and sometimes types that need to be converted to Zod. It's all quite verbose and janky.
I quite like the approach of Typia (uses build-time inline of JavaScript), but it's not compatible with all build chains and questions abound on its viability post Go refactor.
> we end up with a mix of both Zod and types and sometimes types that need to be converted to Zod
In my code, everything is a Zod schema and we infer interfaces or types from the schemas. Is there a place where this breaks down?
Not that I know of aside from code comments (which I like), but I much prefer writing TypeScript to Zod
Could you please elaborate on "patterns for your interfaces"?
If Typescript is javascript with types bolted on, Rescript is javascript with types the way it should have been. Sound types with low complexity. https://rescript-lang.org/
That syntax is so alien to me as a JS/TS developer. I mean coffeescript was as well until JS slowly introduced it all.
It would be cruel for me to force ReScript onto the team because they'd all need to reskill. I could only use it for a private project and then hire exclusively for it afterwards
Really, that is surprising to hear. There are a couple of differences but most of the syntax looks the same to me, what part do you find alien?
The reskill problem is of similiar difficulty with learning a new framework I think. Especially because the language is rather simple compared to typescript (which is also its strength).
I do understand it is an uphill battle. The whole nobody get's fired for choosing IBM thing. The language is still unproven in the general perception. I do think that when it comes to libraries and frameworks I see a lot of developers choose new unproven stuff, more then they do languages.
Does this have a relation to Reason/Reason ML?
“ReScript is a rebranding of BuckleScript and Reason”
https://v11.rescript-lang.org/docs/manual/latest/migrate-to-...
https://rescript-lang.org/blog/bucklescript-is-rebranding/
Isn't that not necessarily out of the ordinary though? What if there's a cosmic ray that change's the value to something not expected by the exhaustive switch? Or more likely, what if an update to a dynamic library adds another value to that enum (or whatever)? What some languages do is add an implicit default case. It's what Java does, at least: https://openjdk.org/jeps/361
> What if there's a cosmic ray that change's the value to something not expected by the exhaustive switch?
I could forgive that.
The TypeScript case is more like "what if instead of checking the types we just actually don't check the types?".
This is how all static type checking works. What programming language do you have in mind that does static type checking and then also does the same type checking at runtime? And what would you expect this programming language to do at runtime if it finds an unexpected type?
I think the point is that other languages make guarantees that ensure you don't have to do any runtime checking. In TypeScript, it's far too easy (and sometimes inevitable) to override the type checker, so some poor function further down into the codebase might get a string when it expects an object, even though there are no type errors.
Compiler exhaustion is such a useful feature. I can’t believe TS doesn’t have it.
TypeScript is neither sound nor complete and was defined that way, beating out other competitors that were sound and/or complete.
What I mean to say is - TypeScript isn't proof.
> The fact that there can be runtime type errors that were proven impossible at compile time is why I will never enjoy TypeScript.
The "impossibility" is just a trait of the type definitions and assertions that developers specify. You don't need to use TypeScript to understand that impossibilities written in by developers can and often are very possible.
My first introduction to TypeScript was trying to use it to solve Advent of Code.
I wrote some code that iterated over lines in a file or something and passed them to a function that took an argument with a numeric type.
I thought this would be a great test to show the benefits of TypeScript over plain JavaScript: either it would fail to compile, or the strings would become numbers.
What actually happened was it compiled perfectly fine, but the "numeric" input to my function contained a string!
I found that to be a gross violation of trust and have never recovered from it.
EDIT: See https://news.ycombinator.com/item?id=46021640 for examples.
> I thought this would be a great test to show the benefits of TypeScript over plain JavaScript: either it would fail to compile, or the strings would become numbers.
You're just stressing that you had a fundamental misunderstanding of the language you were using when you first started to use it. This is not a problem with the language. You simply did not understood what you were doing.
For starters, TypeScript does not change the code you write, it only helps you attach type information to the code you write. The type information you add is there to help the IDE flag potential issues with your code. That's it.
See how node introduced support for running TypeScript code: it strips out type info, and runs the resulting JavaScript code. That's it.
If your code was failing to tell you that you were passing strings where you expected numbers, that was a bug in your code. It's a logic error, and a type definition error.
Static code analysis doesn't change the way you write code. That's your responsibility. Static code analysis adds visibility to the problems you're creating for yourself.
No tool is perfect. What matters is if a tool is useful. I've found TypeScript to be incredibly useful. Is it possible to construct code that leads to runtime type errors? Yes. Does it go a long way towards reducing runtime type errors? Also yes.
> No tool is perfect. What matters is if a tool is useful
Some tools are more perfect and more useful than others.
Typescript's type system is very powerful, but without strict compile-time enforcement you still spend a lot of effort on validating runtime weirdness (that the compiler ought to be able to enforce).
> Typescript's type system is very powerful, but without strict compile-time enforcement you still spend a lot of effort on validating runtime weirdness (that the compiler ought to be able to enforce).
That's something that you own and control, though. Just because TypeScript allows developers to gently onboard static type checking by disabling or watering down checks, that does not mean TypeScipt is the reason you spend time validating your own bugs.
> Just because TypeScript allows developers... does not mean TypeScipt is the reason you spend time validating your own bugs
Unfortunately, taking an ecosystem-wide view, it means exactly that. If one of my dependencies hasn't provided type stubs, or has provided stubs, but then violated their own type signatures in some way, I'm on the hook for the outputs not matching the type annotations.
In a strict language, The compiler would assert that the dependency's declared types matched their code, and I'd only be on the hook for type violations in my own code.
Yes that's true, but there's effort to consider on both sides of design decisions like those TypeScript has made. Much of the compile time behaviour comes from the decision for TypeScript to be incremental on top of JavaScript. That allows you to start getting the benefit of TS without the effort of having to rewrite your entire codebase, for example. Having used TS for many years now I feel that the balance it strikes is incredibly productive. Maybe for other folks/projects the tradeoff is different - but for me I would hate going back to plain JS, and there's no alternative available with such tight integration with the rest of the web ecosystem.
Have you seen ReScript? Of course it is not as popular as typescript but it improves on all the bad parts of typescript. You'll get sound types with the pain points of javascript stripped out. Because it compiles to readable javascript you are still in the npm ecosystem.
You don't have to rewrite your whole codebase to start using it. It grows horizontally (you add typed files along the way) compared to typescript which grows vertically (you enable it with Any types).
The point is that we don't have to move back to plain js. We have learned a lot since typescript was created and I think the time has come to slowly move to a better language (and ReScript feels the most like Javascript in that regard).
It's way better than having to write untyped JavaScript though
That scenario is usually either misuse of escape hatches (especially at API boundaries) or a misunderstanding of what Typescript actually guarantees.
Not really, I provided these examples a couple weeks ago on another HN thread. TypeScript is simply unsound.
https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAllApg...
https://www.typescriptlang.org/play/?#code/DYUwLgBAHgXBB2BXA...
Aren't these bugs that could be "simply" reported and fixed? Or maybe those would get a label "not a bug" attached by the TS creators for some reason?
Both are by design. Array covariance is a common design mistake in OOP languages, which the designer of TypeScript had already done for C# but there they at least check it at runtime. And the latter was declared not-a-bug already IIRC.
TypeScript designers insist they're ok with it being unsound even on the strictest settings. Which I'd be ok with if the remaining type errors were detected at runtime, but they also insist they don't want the type system to add any runtime semantics.
"By design", for me, doesn't say that it can't be changed — maybe the design was wrong, after all. Would it be a major hurdle or create some problems if fixed today?
Perfect examples of the kind of thing I'm talking about, thank you.
In the first example you deliberately create an ambiguous type, when you already know that it's not. You told the compiler you know more than it does. The second is a delegate, that will be triggered at any point during runtime. How can the compiler know what x will be?
First example: you're confusing the annotation for a cast, but it isn't; it won't work the other way around. What you're seeing there is array covariance, an unsound (i.e. broken) subtyping rule for mutable arrays. C# has it too but they've got the decency to check it at runtime.
Second example: that's the point. If the compiler can't prove that x will be initalised before the call it should reject the code until you make it x: number|undefined, to force the closure to handle the undefined case.
For the first one, the compiler should not allow the mutable list to be assigned to a more broadly typed mutable list. This is a compile error in kotlin, for example
> The second is a delegate, that will be triggered at any point during runtime. How can the compiler know what x will be?
x is clearly defined to be a number. The compiler should produce an error if the delegate captures x before it has a value assigned.
If it only works when you write the types correctly with no mistakes, what's the point? I thought the point of all this strong typing stuff was to detect mistakes.
Because adding types adds constraints across the codebase that detect a broader set of mistakes. It's like saying what's the point of putting seatbelts into a car if they only work when you're wearing them - yes you can use them wrong (perhaps even unknowingly), but the overall benefit is much greater. On balance I find that TypeScript gives me huge benefit.
Same here, you can also use the same function in switch cases in Angular templates for the same purpose. Had no idea you could achieve similar with `satisfies`, cool trick.
That's great, I'm going to use that one in the future.
That's very clever!
We have this nifty util in our codebase:
```ts
/*
* A function that asserts that a value is never.
* Useful for exhaustiveness checks in switch statements.
*/
export function assertNever(x: never): never {
}```
I would highly recommend the ts-pattern [1] library if you find yourself wanting exhaustive switch statements! The syntax is a bit noiser than case statements in simple cases, but I find it less awkward for exhaustive pattern matching and much harder to shoot yourself in the foot with. Once you get familiar with it, it can trim down a /lot/ of more complicated logic too.
It also makes match expressions an expression rather than a statement, so it can replace awkward terenaries. And it has no transitive dependencies!
[1]: https://github.com/gvergnaud/ts-pattern
https://typescript-eslint.io/rules/switch-exhaustiveness-che... if that is something you're not aware of!
Nice. I didn’t know I can now replace my “assertExhaustive” function.
Previously you could define a function that accepted never and throws. It tells the compiler that you expect the code path to be exhaustive and fixes any return value expected errors. If the type is changed so that it’s no longer exhaustive it will fail to compile and (still better than satisfies) if an invalid value is passed at runtime it will throw.
I thought the same thing. I also have an assert function I pull in everywhere, and this trick seemed like it would be cleaner (especially for one-off scripts to reduce deps).
But unfortunately, using a default clause creates a branching condition that then treats the entire switch block as non-exhaustive, even though it is technically exhaustive over the switch target. It still requires something like throwing an exception, which at that point you might as well do 'const x: never = myFoo'.
I still keep my assertNever function because it will handle non-exhaustiveness at runtime.
This is what I do:
Including an error message and an error type helps if one does slip through to runtime. Additionally, the AbsurdError can be caught and escalated appropriately. And finally the absurd function can be used in an inline ternary etc. where alternatives like throw cannot.TIL.
Yeah, the satisfies operator is powerful. One thing it allows you to do is to check that one expression is assignable to a type, without actually declaring or coercing the expression to be of that type.
In my experience, here is summary of when you might want to use the various type features:
- Using the "as" keyword tries to coerce/cast an expression to be of some type. This is the least safe, since it overrides proper type checking, at least to some extent. However, in many cases it still does some level of type checking, and it is sometimes necessary if you know something the type checker is unable to prove.
- Declaring an expression to be of some type before initializing it. This is the strictest, and thus most safe, but sometimes it is inconvenient. You might want types to be inferred, which is a really useful thing sometimes, e.g., in a fluent interface when each step constrains types in additional ways.
- The satisfies keyword bridges the gap between the above two: you want to assert/check that the expression is assignable to a certain type (which can be as loose or strict as you want it), whilst still retaining the original type that is inferred for the expression. Unlike "as", you are not overriding the type system. And you are also not actually declaring a type for the expression. I have found this very useful on numerous occasions.
Having said that, there is weird (new?) behavior of the satisfies keyword where appending it to an expressions can cause the the expression itself to fail type-checking internally, where it would otherwise be fine.
This is strange, the basic premise of satisfies is to check if an expression satisfies a type. If there is a type violation at all, it should be at the expression/type boundary, not inside the expression.
It's making it less useful for me.
> TypeScript is a wonderfully advanced language though it has an unfortunately steep learning curve
An extremely steep one.
The average multi-year TypeScript developer I meet can barely write a basic utility type, let alone has any general (non TypeScript related) notion of cardinality or sub typing. Hell, ask someone to write a signature for array flat, you'd be surprised how many would fail.
Too many really stop at the very basics.
And even though I consider myself okay at TypeScript, the gap with the more skilled of my colleagues is still impressively huge.
I think there's a dual problem, on one side type-level programming isn't taken seriously by the average dev, and is generally not nurtured.
On the other hand, the amount of ideas, theory, and even worse implementation details of the TypeScript compiler are far from negligible.
Oh, and it really doesn't help that TypeScript is insanely verbose, this can easily balloon when your signatures have multiple type dependencies (think composing functions that can have different outputs and different failures).
> Hell, ask someone to write a signature for array flat, you'd be surprised how many would fail.
To be clear, an array flat type:
is far from basic Typescript. The average Typescript dev likely doesn't need to understand recursive conditional types. It's a level of typescript one typically only needs for library development.Not only have I never been expected to write something like this for actual work, I'm not sure it's been useful when I have, since most of my colleagues consider something like this nerd sniping and avoid touching/using such utilities, even with documentation.
If I saw that in a PR I would push very hard to reject; something like that is a maintenance burden that probably isn’t worth the cost, and I’ve been the most hardcore about types and TypeScript of anyone of any team I’ve been on in the past decade or so.
Now, that said, I probably would want to be friends with that dev. Unless they had an AI generate it, in which case the sin is doubled.
I think there’s a difference between what’s expected/acceptable for library code vs application code. Types like this might be hard to understand, but they create very pleasant APIs for library consumers. I’ve generally found it very rare that I’ve felt the need to reach for more complex types like this in application code, however.
RXJS’s pipe function has a pretty complex type for its signature, but as a user of the library it ‘just works’ in exactly the type-safe way I’d expect, without me having to understand the complexity of the type.
> If I saw that in a PR I would push very hard to reject; something like that is a maintenance burden that probably isn’t worth the cost
As someone who came from a CS background, this kind of attitude is deeply mysterious. That seems like a type expression I'd expect a CS undergrad to be able to write - certainly if an SDE with 1-2 years experience was confused by it, I'd be advocating against their further promotion.
The every day practice of software engineering has little to do with the academic discipline of computer science. What makes a good software engineer is not usually the same thing that makes a good CS major
Sure, but basic CS knowledge is an expectation in much of the software field (albeit less since the mid-2010's javascript boom). A lot of companies aren't going to hire you if you don't know the basics of data structures and algorithms
but then you wind up with an entire repo, or an entire engineering team utterly hobbled by a lack of expressive typing (or advanced concepts generally) and debased by the inelegance of basic bitch programming.
If it's correct, it's not a maintenance nightmare, and it will alert you to problems later when someone wants to use it incorrectly.
If you're writing first-party software, it probably doesn't matter. But if you have consumers, it's important. The compiler will tell you what's wrong all downstream from there unless someone explicitly works around it. That's the one you want to reject.
> If it's correct, it's not a maintenance nightmare, and it will alert you to problems later when someone wants to use it incorrectly.
You're confusing things. It is a maintenance nightmare because it is your job to ensure it is correct and remains correct in spite of changes. You are the one owning that mess and held accountable for it.
> If you're writing first-party software, it probably doesn't matter. But if you have consumers, it's important.
Yes, it is important that you write correct and usable code. That code doesn't fall on your lap though and you need to be the one writing and maintaining it. Whoever feels compelled to write unintelligible character soup that makes even experienced seasoned devs pause and focus is failing their job as a software engineer.
> Whoever feels compelled to write unintelligible character soup...
I see it differently. That's the name of the game. Language design is always striving toward making it more intelligible, but it is reasonable to expect pros to have command of the language.
> I see it differently. That's the name of the game. Language design is always striving toward making it more intelligible, but it is reasonable to expect pros to have command of the language.
No, that's an extremely naive and clueless opinion to have. Any basic book on software engineering will tell you in many, many ways that the goal of any software engineer is to write simple code that is trivial to parse, understand, and maintain, and writing arcane and overly complex code is the Hallmark of an incompetent developer. The goal of a software engineer is to continuously fight complexity and keep things as simple as they can be. Just because someone can write cryptic, unintelligible code that doesn't make them smart or clever: it only makes them bad at their job.
I’d say it depends. I always advocate for code that is easy to read and to understand, but in extremely rare conditions, hard to read code is the better solution.
Especially when it comes to signatures in Typescript, complex signatures can be used to create simple and ergonomic APIs.
But anyway you shouldn’t be allowed to push anything like this without multiple lines of comments documenting the thing. Unreadable code can be balanced with good documentation but I rarely saw this unfortunately.
Comments and tests. Vitest supports unit tests for types.
looking back at them is also real hard to debug. you dont get a particularly nice error message, and a comment or a test would tell better than the type what the thing should be looking like
What's the alternative? Have incorrect types for the function? That's not better.
The alternative is what shows in the comment: go on HN and tell the world you think TS and JS are crap and it's not worth your time, while writing poor software.
To answer this we probably need more details, otherwise it's gonna be an XY Problem. What is it that I'm trying to do? How would I type this function in, say, SML, which isn't going to allow incorrect types but also doesn't allow these kinds of type gymnastics?
We don't have to deal in hypotheticals - we have a concrete example here. There's a method, array.flat() that does a thing that we can correctly describe in TypeScript's type system.
You say you would reject those correct types, but for what alternative?
It's hugely beneficial to library users to automatically get correctly type return values from functions without having to do error-prone casts. I would always take on the burden of correct types on the library side to improve the dev experience and reduce the risk of bugs on the library-consumption side.
There's nothing I can do about the standard JavaScript library, but in terms of code I have influence over, I very simply would not write a difficult-to-type method like Array.prototype.flat(), if I could help it. That's what I mean by an XY Problem - why are we writing this difficult-to-type method in the first place and what can we do instead?
Let's suppose Array.prototype.flat() wasn't in the standard library, which is why I'm reviewing a PR with this gnarly type in it. If I went and asked you why you needed this, I guess you'd say the answer is: "because JavaScript lets me make heterogenous arrays, which lets me freely intermix elements and arrays and arrays of arrays and... in my arrays, and I'm doing that for something tree-like but also need to get an array of each element in the structure". To which I'd say something like "stop doing that, this isn't Lisp, define an actual data type for these things". Suddenly this typing problem goes away, because the type of your "flatten" method is just "MyStructure -> [MyElements]".
Sure, if you're living fully in your own application code, and you don't need to consume things from an API you don't control, it's easy to live in a walled garden of type purity.
I can recognize that most people are going to go for inaccurate types when fancier semantics are necessary to consume things from the network.
But we also have the real world where libraries are used by both JS devs and TS devs, and if we want to offer semantics that idiomatic for JS users (such as Array.prototype.flat()) while also providing a first-class experience to TS consumers, it is often valuable to have this higher-level aptitude with the TS type system.
As mentioned earlier, I believe 90% of TS devs are never in this position, or it's infrequent enough that they're not motivated to learn higher-level type mechanics. But I also disagree with the suggestion that such types should be avoided because you can always refactor your interface to provide structure that allows you to avoid them; You don't always control the shape of objects which permeate software boundaries, and when providing library-level code, the developer experience of the consumer is often prioritized, which often means providing a more flexible API that can only be properly typed with more complex types.
> Suddenly this typing problem goes away, because the type of your "flatten" method is just "MyStructure -> [MyElements]".
How is that less maintenance burden than a simple Flatten type? Now you have to construct and likely unwrap the types as needed.
And how will you ensure that you're flattening your unneeded type anyways? Sure you can remove the generics for a concrete type but that won't simplify the type.
It's simple. It's just recursive flattening an array in 4 lines. Unlikely to ever change, unlike the 638255 types that you'd have to introduce and maintain for no reason.
There are many reasons not to do that. Say your business logic changes and your type no longer needs one of the alternatives: you are unlikely to notice because it will typecheck even if never constructed and you will have to deal with that unused code path until you realize it's unused (if you ever do).
You made code harder to maintain and more complex for some misguided sense of simplicity.
> MyStructure -> [MyElements]
Right, from the structure you get an array with one element which is likely an union type from that naming.
Honestly, you sound more like your arguing from the perspective of a person unwilling to learn new things, considering you couldn't even get that type correct.
To begin with, that flat signature wasn't even hard to understand?
What I wrote would be a syntax error in TypeScript (no name for the argument, wrong arrow), not a function that returns array with one element; I used Haskell-ish notation instead of TypeScript's more verbose "(structure: MyStructure) => MyElement[]".
I thought it was clear enough that I was being informal and what I meant was clear, but that was admittedly probably a mistake. But to infer an implication from that that I'm "unwilling to learn new things" is a non sequitur and honestly kind of an unnecessarily dickish accusation.
Brah, If you have a type with that many characters in it that isn’t a super long string name, it’s not easy to understand unless you are the 1% of 1% when it comes to interpreting this specific language.
On top of that I fully agree with the poster you’re responding to. In general application code that’s and extremely complicated type, generally done by someone being as clever as can be. And if the code you’ve written when you’re being as clever as possible has a bug in it, you won’t be clever enough to debug it.
This. 1000%.
The version I was thinking when I wrote the comment is simpler
> The average Typescript dev likely doesn't need to understand recursive conditional types.The average X dev in Y language doesn't need to understand Z is a poor argument in the context of writing better software.
> The average X dev in Y language doesn't need to understand Z is a poor argument in the context of writing better software.
It's a good response to the claim that we'd be surprised at how many would fail to do this, though.
as a person that never touched JS and TS... what's the difference between the two answers?
For one, the simple answer is incomplete. It gives the fully unwrapped type of the array but you still need something like
The main difference is that the first, rest logic in the complex version lets you maintain information TypeScript has about the length/positional types of the array. After flattening a 3-tuple of a number, boolean, and string array TypeScript can remember that the first index is a number, the second index is a boolean, and the remaining indices are strings. The second version of the type will give each index the type number | boolean | string.First one flattens a potentially-nested tuple type. E.g., FlatArr<[number, [boolean, string]]> is [number, boolean, string].
Second one gets the element type of a potentially-nested array type. E.g., Flatten<number[][]> is number.
For what it's worth, I've never needed to use either of these, though I've occasionally had other uses for slightly fancy TypeScript type magic.
The answer above actually gets the type union of all non-array elements of a multi-level array.
In other words
will give you a union type of 1, 2, 'a', and 'b' Technically the inference is unnecessary there, if that's you're goal: I don't really consider this the type of flattening an array, but `Array<Flatten<ArrType>>` would be. And this would actually be comparable to the builtin Array.prototype.flat type signature with infinite depth (you can see the typedef for that here[1], but this is the highest level of typescript sorcery)My solution was for flattening an array with a depth of 1 (most people using Array.prototype.flat are using this default depth I'd wager):
The type I provided would match those semantics: [1]: https://github.com/microsoft/TypeScript/blob/main/src/lib/es...Here’s the fun part that I suspect many here are forgetting: if you want to write the function body, it will probably (or at the very least can) look very similar!
If you want to unternarise it: I still wouldn’t call it basic TypeScript, but it’s not conceptually that advanced, you just need to know about infer and extends.Now in reality, Array.prototype.flat has a more complex definition, partly because (like most of Array’s methods) the method is generic (it works on array-like objects that have a length property and numeric indexing), and partly because of the depth parameter. From lib.es2019.array.d.ts:
Ouch. Don’t like the { done, recur }[Depth extends -1 ? "done" : "recur"] at all, no idea why it wasn’t written as `Depth extends -1 ? Arr : Arr extends ReadonlyArray<…`. And as for hard-coding support for depths up to 20 then bailing… probably pragmatic, it’s possible to support all values, but rather messy: https://stackoverflow.com/q/54243431.I recently had to write a Promise.all, but using an object instead of an array.
That was... non-trivial.
If it's what I'm thinking, that one isn't too bad. I wrote it awhile back:
I'd call that bad pretty bad.
Without internet or AI I wouldn't attempt writing anything like that.
rejoice https://github.com/tc39/proposal-await-dictionary
For those unfamiliar with TS, the above is just...
...in TS syntax.Well, it is the type of that, in TS syntax. Few are the statically-typed languages that can even express that type.
Java: List<Object>
Python: list[Any]
...what am I missing?
You're missing the specialisation of Object/Any. For example Array.flat called with [int, [bool, string]] returns a type [int, bool, string]. Admittedly this is somewhat niche, but most other languages can't express this - the type information gets erased.
You're missing the input type, essentially. Those are just array types. The TypeScript type signature more of a function type, it expresses flattening a n-dimensional array (input type) into a flat array (output type).
> Too many really stop at the very basics.
I don’t think that means it has a steep learning curve. It just means the basics suffice for a ton of TypeScript deployments. Which I personally don’t see as the end of the world.
Yes, to me this is a biggest feature of Typescript: A little goes a long way, while the advanced features make really cool things possible. I tend to think of there being two kinds of Typescript - Application Typescript (aka The Basics, `type`, `interface`, `Record`, unions etc...) and Library Typescript which is the stuff that eg Zod or Prisma does to give the Application Typescript users awesome features.
While I aspire to Library TS levels of skill, I am really only a bit past App TS myself.
On that note I've been meaning to the the Type-Level Typescript course [0]. Has anyone taken it?
https://type-level-typescript.com/
> Too many really stop at the very basics.
As someone who knows slightly more than the basics, and enough to know about the advanced stuff that I don't know about, this is the correct place to stop.
I would much rather restructure my javascript than do typescript gymnastics to fit it into the type system.
I agree. The advanced stuff mostly exists in order to allow writing type annotations for JavaScript libraries that have APIs that are very dynamic.
If you're purely writing Typescript then you mostly don't need it.
[flagged]
You can restructure your JS to avoid some crazy verbose TS though, sometimes. I think that's the point they were making. Why be so hostile?
It's also terribly documented. As an example, I don't think `satisfies` is in the docs outside of release notes. There's lots more stuff like that, which makes using it kind of frustrating.
Preach. It’s astonishing how much necessary documentation lives as unchanging footnotes on TS releases.
https://www.google.com/search?q=site%3Ahttps%3A%2F%2Fwww.typ...
Typescript types often devolve into
keyof typeof[number] ? uppercase (myNum)
On the other hand, there isn't much Omit, readonly, mutable in existing code bases, so devs have nowhere to learn but documentation.
Then the ground shifts again and –what should be basic stuff – enums are banned, because erasableSyntaxOnly makes life so much easier.
Being a JS/TS one-trick pony all my career, how does it compare to other languages? I don't really see much difference, except if comparing with some C++ shenanigans.
"There basics," well understood and judiciously applied, is where the bulk of TypeScript's value lies.
> "There basics," well understood and judiciously applied, is where the bulk of TypeScript's value lies.
Yes, precisely. OP is also completely oblivious to the fact that TypeScript is designed to help developers gradually onboard legacy JavaScript projects and components, which definitely don't require arcane and convoluted type definitions to add value.
This is the bell curve meme, and you are in the middle telling us "template metaprogramming in C++ is amazing".
these are things most developers don't know how to do in most language's type systems. I think only rust with its focus on functional roots has seen similar focus on utilizing its type system to its fullest extent.
TypeScript codebases I've seen generally seem to have the widest demonstration of skill gap versus other languages I use.
For example, I don't ever see anyone using `dynamic` or `object` in C#, but I will often see less skilled developers using `any` and `// @ts-ignore` in TypeScript at every possible opportunity even if it's making their development experience categorically worse.
For these developers, the `type` keyword is totally unknown. They don't know how to make a type, or what `Omit` is, or how to extend a type. Hell, they usually don't even know what a union is. Or generics.
I sometimes think that in trying to just be a superset of JavaScript, and it being constantly advertised as so, TypeScript does not/did not get taken seriously enough as a standalone language because it's far too simple to just slot sloppy JavaScript into TypeScript. TypeScript seems a lot better now of having a more sane tsconfig.json, but it still isn't strict enough by default.
This is a strong contrast with other languages that compile to JavaScript, like https://rescript-lang.org/ which has an example of pattern matching right there on the home page.
Which brings me onto another aspect I don't really like about TypeScript; it's constantly own-goaling itself because of it's "we don't add anything except syntax and types" philosophy. I don't think TypeScript will ever get pattern matching as a result, which is absurd, because it has unions.
> For example, I don't ever see anyone using `dynamic` or `object` in C#, but I will often see less skilled developers using `any` and `// @ts-ignore` in TypeScript at every possible opportunity even if it's making their development experience categorically worse.
I think you're confusing things that aren't even comparable. The primary reason TypeScript developers use the likes of `any` is because a) TypeScript focuses on adding support for static type checking on a language that does not support it instead of actually defining the underlying types, b) TypeScript developers mostly focus on onboarding and integrating TypeScript onto projects and components that don't support it, b) TypeScript developers are paid to deliver working projects, not vague and arbitrary type correctness goals. Hence TypeScript developers tend to use `any` in third party components, add user-defined type guards to introduce typing in critical areas, and iterate over type definitions when time allows.
On the other hand, would we even be talking about it if it hadn't stuck to its goals?
It will get pattern matching when JS does. Not certain yet but in progress.
https://github.com/tc39/proposal-pattern-matching
That proposal is really dragging though. And typescript needs as much work because that's where the real power is. We need discern thing like
with exhaustiveness checking for it to be truly useful.Discriminating a function or promise based on return type is never going to work, because JavaScript is dynamically typed and TypeScript erases types at compile time, so there's no way to know at runtime what type a function or promise is going to return.
It'll work because that's what typescript does, and that's why it needs to be implemented there, also. That's my point.
And as far as runtime goes, well, that's not what typescript does. It's a typical compile-time static type system.
Typescript aside, even a javascript-level first-class pattern expression is still extremely useful. I really hope it gets in there soon.
> For example, I don't ever see anyone using `dynamic` or `object` in C#
I have bad news for you
> The average multi-year TypeScript developer I meet can barely write a basic utility type, let alone has any general (non TypeScript related) notion of cardinality or sub typing. Hell, ask someone to write a signature for array flat, you'd be surprised how many would fail.
I think you're both exaggerating your blanket accusations of incompetence and confusing learning curve with mastering extremely niche techniques akin to language gotchas.
to our defense, we want to build stuff not become ts wizards. also I've worked with libraries with heavy heavy typing that it was a nightmare if you wanted to use their lib in any other way than what they have imagined.
You're right, but that begs the question: does a type system really require such complexity?
I'm aware that type theory is a field in and of itself, with a lot of history and breadth, but do developers really need deep levels of type flexibility for a language to be useful and for the compiler to be helpful?
I think TypeScript encourages "overtyping" to the detriment of legibility and comprehension, even though it is technically gradually typed. Because it is so advanced and Turing complete itself, a lot of brain cycles and discussion is spent on implementing and understanding type definitions. And you're definitely right that it being verbose also doesn't help.
So it's always a bittersweet experience using it. On one hand it's great that we have mostly moved on from dynamically typed JavaScript, but on the other, I wish we had settled on a saner preprocessor / compiler / type system.
The idea is to make libraries preserve as much type information as possible, as a principle. Once type information is erased it can't be restored. For regular application code you don't need to use those features.
But regular application code also contains libraries. Type information is useful even if you're the only user of those APIs.
My point was more related to the level of expressiveness required of a type system in order to allow a programmer to produce reliable code without getting in their way. I think TypeScript leans more towards cumbersome than useful.
For example, I'm more familiar with Go's type system, which is on the other side of that scale. It is certainly much less expressive and powerful than TypeScript, and I have found it frustrating and limiting in many ways, but in most day-to-day scenarios it's reasonably adequate. Are Go programs inherently worse off than TypeScript programs? Does a Go programmer have a worse experience overall? I would say: no.
>And even though I consider myself okay at TypeScript, the gap with the more skilled of my colleagues is still impressively huge.
Maybe they're smart, but the even smarter dev would avoid unnecessary complexity in the first place.
> ask someone to write a signature for array flat
Out of curiosity - what do you think is a satisfactory answer here?
My answer would vary wildly based upon more details, but at the most basic all I can think you could guarantee is Array<unknown> => Array<unknown>?
In an interview I'd be happy just seeing some reasoning.
IRL I'd be happy with someone at least searching for a definition and trying to learn from it.
I've asked this question multiple times as implementing array flatten used to be our go to ice breaker question, and many devs had no issues reasoning and finding an okay type definition.
Not steep so much as deep.
There’s a lot you can do in TypeScript. But you don’t have to do it. And TS existed successfully a long time without those features.
Honestly I just use TypeScript to prevent `1 + [] == "1"` and check that functions are called with arguments. I don't care about type theory at all and the whole thing strikes me as programmers larping (poorly) as mathematicians.
I couldn't care less about mathematics, but I do care about making impossible state impossible and types documenting the domain.
If you type some state as:
then you're creating a giant mess of a soup where the state of your program could have a result, be loading and an error at the same time. If you could recognise that the state of your program is a sum of possible states (loading | success | error), and not their product as the type above you could highly simplify your code, add more invariants and reduce the number of bugs.And that is a very simple and basic example, you can go *much* further, as in encoding that some type isn't merely a number through branded types, but a special type of number, be it a positive number between 2 and 200 or, being $ or celsius and avoiding again and entire class of bugs by treating everybody just as an integer or float.
You can encode that "correctly" in pure JS
Generally it's a bad pattern to have dependant fields like your example, but you don't need TS to solve it.Of course you can do it in JS too.
But you can't reason on the different cases aided by the type checker.
Also, using classes or POJOs is merely an implementation detail.
Thats OOP, we don't do that anymore!!! /s
>encoding that a number is between 2 and 200
What’s the point of this level of autism when you still have to add run time checks?
For a function setVelocity() that can accept 1..<200. You call it with numbers that you enter directly and types tell you something that would otherwise be a comment on the function, or you do runtime checks elsewhere, and the type becomes proof that you checked them before handing it into the function.
Btw, using “autism” to mean “pedantry” leaves a bit of a bad taste in my mouth. Maybe you could reconsider using it that way in the future.
Pushing everything to types like this creates a different burden where you're casting between types all over the place just to use the same underlying data. You could just clamp velocity to 200 in the callee and save all that hassle.
> Pushing everything to types like this creates a different burden where you're casting between types all over the place just to use the same underlying data.
TypeScript does not perform any kind of casting at all. What TypeScript supports is structural typing, which boils down to allowing developers to specify type hints in a way that allows the TypeScript compiler to determine which properties or invariants are met in specific code paths.
Literal types address a very common and very mundane use case: assert what can and cannot be done with an object depending on what value one of it's fields have.
Take for example authorization headers. When they are set, their prefix tells you which authorization scheme is being used by clients. With typescript you can express those strings as a prefix constrained string type, and use them to have the TypeScript compiler prevent you from accidentally pass bearer tokens to the function that handles basic authentication.
Literal types shine when you are using them to specify discriminant fields in different types. Say for example you have a JSON object that has a `version` field. With literal types you can define different types discriminated by what string value features in it's `version` field, and based on that alone you can implement fully type-safe code paths.
Casting? Not really - i think you’d only need a couple type checks.
Imo this is mostly useful for situations where you want to handle input validation (and errors) in the UI code and this function lives far away from ui code.
Your point about clamping makes sense, and it’s probably worth doing that anyway, but without it being encoded in the type you have to communicate how the function is intended to be used some other way.
What about documentation as in docstring, function signature label and the like. I quite like CommonLisp, EmacsLisp, Go for that.
How would you convert a Number type to a ClampedNumber type without casting?
Ah, yeah you’re right. I somehow thought typescript could do type narrowing based on checks - like say:
If (i >= 1) { // i’s type now includes >= 1 }
But that is not the case, so you’d need a single cast to make it work (from number to ClampedNumber<1,200>) or however exactly you’d want to express this.
Tbf having looked more closely into how typescript handles number range types, I don’t think I would ever use them. Not very expressive or clear. I think I hallucinated something closer to what is in this proposal: https://github.com/microsoft/TypeScript/issues/43505
I still think that the general idea of communicating what acceptable input is via the type system is a good one. But the specifics of doing that with numbers isn’t great in typescript yet.
How would you implement it in other languages that support it better? Can you literally just do a range check and the compiler infers its range for types? If so thats actually pretty neat.
Being guaranteed that _at runtime_ you won't end up with 1 or 201.
Blub.
I have mixed feelings about Typescript, I hate reading code with heavy TS annotations because JS formatters are designed to keep line widths short, so you end up with a confusing mess of line breaks. Pure JS is also just more readable.
Also you can so easily go overboard with TS and design all sorts of crazy types and abstractions based on those types that become a net negative in your codebase.
However it does feel really damn nice to have it catch errors and give you great autocomplete and refactoring tooling.
> I have mixed feelings about Typescript, I hate reading code with heavy TS annotations because JS formatters are designed to keep line widths short, so you end up with a confusing mess of line breaks. Pure JS is also just more readable.
That's not a TypeScript issue, it's a code quality issue and a skill issue. Anyone can put together an unintelligible mess in any language.
typescript is largely a result of solving a non-existent problem. Yeah JS is finicky & has foot-guns, however they're ways around those foot guns that don't involve typescript.
Rich Hickey in 10 Years of Clojure & Maybe Not then the Value of Values - lays this out - though not meant at typescript but static types in general.
the thing most people don't have proper Javascript fundamentals.
Function signatures: JSDoc works
Most types - use Maps | Arrays
if a value doesn't exist in a map we can ignore it. There's also the safe navigation operator.
Instead of mutable objects - there's ways around this too. Negating types again.
> typescript is largely a result of solving a non-existent problem.
Claiming that lack of static type checking is a non-existent problem is quite a bold claim. Care to offer some details to substantiate your claim?
Uh, among several other issues with this, what use are JSDoc comments for typing, without typescript to check them?
In my personal projects, I’m a fan of using satisfies to check Zod definitions against the interfaces they validate.
I find the base interfaces easier to read at a glance than derived types, especially in an editor’s hover view.
Though, nullable fields might get weird, iirc.
Yeah, I believe that doesn't quite work correctly for nullable fields or cases where the Zod type would be a subtype of the declared type. But it's a really useful technique, because it's a lot easier to work with types you've declared in TypeScript than the ones Zod generates. I'm sure there's scope for a validation library that is designed around the user providing a TypeScript type and then producing an error if the validation doesn't match that type.
Satisfies is very useful as a library author for testing your types. You can write `typetest` files that don't become part of the bundle but are compiled.
One example is that we have a TS library for a JSON-RPC server that is horribly complex on the server side, returning different shapes of output based on input params. I don't think it can be typed with OpenAPI etc, but it can be with Typescript method overloads.
But then instead of writing unit tests that either coerce or mock the server into returning each shape, we can write typetests like this to make sure our type juggling provides the exact right types to developers:
```The tradeoff is slightly longer compile times for the library since all this is checked at build time, but it provides a lot of flexibility for testing overloads and utility types in isolation.
You can use multiple tsconfig files to remove the compile time issue; this is quite common for test files anyway. Its not unusual to see one for CI & IDE and one for compilation, the compilation one having a much more limited scope of files.
The second example confuses me. The Person type has isCool: boolean, not an explicit true. How does using satisfies here pass coolPeopleOnly?
You can sorta think of `satisfies Foo` as "the type is exactly the literal value, but also make sure the value could be used in the place of a Foo"
Why? why make your code so complex you even hit this problem. Just use the type:
const x: Thetype = ....
I am not keen on as const either. Just program to interfaces. It is a better way to think IMO.
The author gets into that. `Thetype` might be complex. It also protects you from overgeneralizing, like casting to and from `unknown` to escape the type checker.
The bad convert is actually wrong. It should be refactored to an equality check and throw an error if account kind is not "dc". The compiler is correct and its not a good idea to work around this issue.
No, the point of the function is to convert the type. It doesn't need to check anything, it just forcibly converts any argument to DC.
The compiler isn't complaining because the conversion isn't valid, it's complaining because it doesn't know that the string "dc" should be narrowed down to a literal type, and so it's kept it as broad as possible. Using `satisfies` lets it understand that it needs to do narrowing here.
In fairness I don't think this is the best case for `satisfies`, and some return type annotations would probably work a lot better here, and be clearer to read.
Absolutely. It's contrived example. If you control the API, you'd be better to rewrite it:
And it would flow more naturally. Or define a CurrentKind and include it in current if you don't need to handle the scalar. There are plenty of better ways. That's also something that you can become accustomed to and recognize when you have a weak API.I'm not really trying to make a case for `satifies`, but it can be handy and clean in some circumstances, especially when you're constrained by a third-party API.
I see. I dont mind this use case as much. It is like a hint.
Exactly. It's a safer assertion, as the author points out. You can work without it, but it's a little more cumbersome and involves creating the otherwise unnecessary, transient intermediate types.
Because what you wrote broadens the type. `satisfies` validates the type without broadening it.
const c: string = 'c';
This will be of type string instead of type 'c'. This is a barebones example and it already breaks support for template literal types. Imagine how much better `satisfies` is for complex union types.
This is wordier than just "as const", what advantage does it give? (I am a newbie and genuinely don't know)
edit: perhaps the advantage only comes into play for mutable values, where you want a narrower type than default, but not that narrow. Indeed, this is covered in the article, but CTRL+F "as const" doesn't work on the page for whatever reason, so I missed it.
The satisfies keyword is quite different than "as const." What it does is:
1. Enforce that a value adheres to a specific type
2. But, doesn't cause the value to be cast to that type.
For example, if you have a Rect type like:
You might want to enforce that some value satisfies Rect properties... But also allow it to have others. For example: If you wrote it as: TypeScript wouldn't allow you to also give it x and y properties. And if you did: at the end of the line, TypeScript would allow the x, y properties, but would immediately lose track of them and not allow you to use them later, because you cast it to the Rect type which lacks those properties. You could write an extra utility type: But that can get quite verbose as you add more fields. And besides: in this example, all we actually are trying to enforce is that the object is a Rect — why do we also have to enforce other things at the same time? Usually TS allows type inference for fields, but here, as soon as you start trying to enforce one kind of shape, suddenly type inference breaks for every other field.The satisfies keyword does what you want in this case: it enforces the object conforms to the type, without casting it to the type.
Then if someone edits the code to: TypeScript will throw an error, since it no longer satisfies the Rect type (which wants h and w, not height and width).This was a fantastic writeup, thanks. If you don't mind an additional question...
How does this work,
Since- person isn't const, so person.isCool could be mutated
- coolPeopleOnly requires that it's input mean not only Person, but isCool = true.
If you ignore the `satisfies` for a moment, the type of `person` is the literal object type that you've written (so in this case, { "person": string, isCool: true }). So coolPeopleOnly(person) works, regardless of whether `satisfies` is there, because TypeScript sees an object literal that has all the person attributes and also `isCool: true`.
(You could mutate it to `isCool: false` later, but then TypeScript would complain because `isCool: false` is different to `isCool: true`. When that happens isn't always obvious, TypeScript uses a bunch of heuristics to decide when to narrow a type down to the literal value (e.g. `true` or `"Jerred"`), vs when to keep it as the more general type (e.g. `boolean` or `string`).)
What `satisfies` is doing here is adding an extra note to the compiler that says "don't change the type of `person` at all, keep it how it is, _but_ also raise an error if that type doesn't match this other type".
(This is only partially true, I believe `satisfies` does affect the heuristics I mentioned above, in that Typescript treats it a little bit like `as const` and narrows types down to their smallest value. But I forget the details of exactly how that works.)
So the `coolPeopleOnly` check will pass because the `person` literal has all the right attributes, but also we'll get an error on the literal itself if we forget an attribute that's necessary for the `Person` type.
Why is satisfies needed at all, when can't. Typescript realize that `a` satisfies `Rect` automatically?
It does; the code will still type-check without the satisfies operator. satisfies lets you say "if this value doesn't conform to this type then I want that to be an immediate compile error, even if it would otherwise be okay". Which isn't needed all that often since usually getting the type wrong would produce a compile error elsewhere, but occasionally it proves useful. When designing the feature they collected some use cases: https://github.com/microsoft/TypeScript/issues/47920
Thanks; succinct and for me, I understood it.
I've really only found benefit on the return type of functions, when you can say that a type parameter satisfies a type (with the return type being a boolean). This let's you use `if (isPerson(foo))` and typescript will narrow the type appropriately in the conditional
With as const you can’t verify against another interface
> This keyword is a bit esoteric and not very common, but it comes in handy in some scenarios where you’d otherwise pull your hair out.
Typescript in a nutshell. That said, satisfies is a good keyword!
Does anyone know what was used to render these code blocks in the article? The mouseover tooltip is extremely cool. I've never seen anything like it before.
EDIT: I dug through the codebase and determined that it's using Shiki and TwoSlash for the syntax highlighting and tooltips.
Meanwhile, Python can sometimes verify if something is an int or a string.
> TypeScript is a wonderfully advanced language though it has an unfortunately steep learning curve; in many ways it’s the complete opposite of Go.
Replace "TypeScript" with "C++" and the same can be said.
It is one of the worst languages ever designed and already built on top of a sloppy foundation (Javascript) compared to Go.
The language encourages escape hatches and tons of flexibility on how it checks its types and creates the risk of inconsistency to engineers on which rules to adopt and there is always one engineer that will disagree with some settings and argue to turn on/off a rule to defeat the purpose of the language.
At this stage, its no better than C++ but significantly slower, and I've seen the same mistakes (enums, allowing "as XYZ" casting, etc) in C++ creeping into TypeScript.
Even the entire language parser and type checker is being rewritten in Go. [0]
[0] https://devblogs.microsoft.com/typescript/typescript-native-...
I disagree. The speed with which Typescript replaced JS as the default is testament to its strength. I remember back is the good old JS only days you had to assume that all the form values we passed and read were strings and not an integer account number for example. Typescript solves these challenges and that alone eliminates a large class of frankly nonsensical bugs.
The reason it is being rewritten in Go is so the compiler can be run at native speeds which is a good thing. Otherwise the only runtime we have available is NodeJS which has its fair share of problems. You can go far with TS with just interface definitions and some generics peppered in for common utilities. The places where all the type gymnastics is necessary are cases I frankly havent encountered in the usual course of development.
Tell me when Go is browser ready
Misses the point. Both these languages have their place. Besides there's no benchmarks beyond developers personal opinions that tells us whether a language is browser ready or not.
When.
So satisfies prevent you from mutating then? Otherwise you could just change name afterwards...
It prevents you from mutating via the reference that you obtain from `satisfies` without casting its type, yes (or rather more precisely, you can mutate it, but only to the one allowed value).
However, the object can still be mutated via other references to it. TypeScript is full of holes like this in the type system - the problem is that they are trying to bolt types and immutability onto a hot mess that is JS data model while preserving backwards compatibility.
I’m so frustrated by satisfies because it eliminates optional properties.
I want an object of ‘LayerConfig’ elements where each key is the name of a possible layer. Without ‘satisfies’ I have to name every layer twice in my config. But with it, I can’t have optional properties (eg. Half the layers are fine with the default values for some properties).
The best I’ve found is a hack that uses a function. But this whole thing where my key literals widen into “string” is a constant annoyance to otherwise very elegant code.
Then either make the properties optional or use Partial on the type you are satisfying
meh article
> Why is the name of person1 of type string and not the literal "Jerred"? Because the object could be mutated to contain any other string.
Not really, if you declare {name: "Jerred" as const}, it's still mutable. Typescript just decided that given certain primitive-like types like strings, it's preferrable to infer string rather than as constant.
Satisfies offers the opposite AS A MOSTLY ORTHOGONAL design decision. It's a happy byproduct that the type inference's behavior is changed.
And this is relevant because it affects technically important situations like deeply nested values NOT being narrowed, but it's also just not a good mental model for what it's supposed to do.
People should assume that given a type literal, that it just infers the widest typing. Incidental behavior that arises from using 'as const', or 'satisfies' should follow it's semantic purpose. If you want specific typing, just build the type - don't use hacks.
Satisfies is useful because sometimes you have something with some typing (often as const for something like utils), that you also need to make sure satisfies some other typing - almost as a constraint.
Would not surprise me if ts team released a keyword that did type inference with narrowest (like as const, but without the readonly).
This article shows the kind of overthinking that I hate when it comes to typescript usage. This is also the reason why I'm falling in love with Go.
Don't get me started on how this is so unnecessary on the browser (so the utility on this kind of stuff is actually for library development; maybe).
Am I the only one that thinks that Typescript waste your time on minutia like this?
[dead]
the cool thing about Typescript is that you never have to know any of this to deliver highly performant enterprise scale software
When you are building even moderately large non-trivial real world software with JS, you would thank your stars that TS exists and that it has features like this. It's amazing how quickly the basic concepts of software engineering become inadequate when they run up against the real world.
For example, a naive engineer might think, ah, databases have this oFFSET clause, good, I'll use, unaware that is a foot gun for real world performance.
Or they may think the DELETE operation on DBs is a normal thing, unaware that in most cases it should NOT be used at all.
Or they might think loops are idiomatic, unaware that when you are writing large software you should and can probably almost eliminate loops (unless you are using a DSL like SQL extensions, config languages, etc)
Or they may be unaware of the thorny issues around queues (or even why they might be necessary in the first place) and concurrent access to data.
Or they might not understand why the dogma of separating content and presentation is nonsensical in many situations.
Etc.
I think this is spot on. TS goes very very far without the gymnastics. Its good to know these things, but not even close to necessary to be productive. Not to mention IDE support for TS is on a very different level conpared to the shitshow that is Javascript.
Also, Angular dev spotted :)
You can do that with assembly and not know ANY high level language.
You might be interested in reading PG's treatise on "the blub paradox".
[dead]
Can you even do that? I though you could only enterprise in secure languages, like Java
2010 you rang?
The language isn’t the bottleneck
80% of the value of TypeScript is that it will tell you when when you changed or added a parameter and forgot to update it everywhere, you doofus. The other 20% is that it keeps coding agents from going too far off the rails. Trying to use the type system as a metaprogramming language is only valuable as a fun exercise, but of negative value in real world projects.
That's a little ungenerous. Richer types are not intrinsically about metaprogramming, they just let you model your domain more accurately so you can turn runtime errors into build-time errors. If your system already has natural constraints, you get to document them in a machine-checkable way.
This thread is weird, also spot on, not sure why its downvoted. I am happy when the IDE spots my dufusness.