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.
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'.
> 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 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.
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]".
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?
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.
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.
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
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.
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.
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 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 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.
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.
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.
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.
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.
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.
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
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);
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.
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.
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.
> 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]
> 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).
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.
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.
That's great, I'm going to use that one in the future.
That's very clever!
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'.
> 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 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.
What's the alternative? Have incorrect types for the function? That's not better.
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]".
> 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?
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.
This. 1000%.
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.
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 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.
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:
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?
> 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.
[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.
>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.
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.
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.
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.
Uh, among several other issues with this, what use are JSDoc comments for typing, without typescript to check them?
This is the bell curve meme, and you are in the middle telling us "template metaprogramming in C++ is amazing".
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.
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.
> 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>?
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.
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.
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.
Blub.
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.
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
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.
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.
I see. I dont mind this use case as much. It is like a hint.
> 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.
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
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.
> 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-...
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).
[dead]
the cool thing about Typescript is that you never have to know any of this to deliver highly performant enterprise scale software
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
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.