Til: `satisfies` Is My Favorite Typescript Keyword
Postedabout 2 months agoActiveabout 2 months ago
sjer.redTechstory
calmpositive
Debate
0/100
TypescriptProgrammingSoftware Development
Key topics
Typescript
Programming
Software Development
The author shares their enthusiasm for the `satisfies` keyword in TypeScript, and the post aims to explore its usefulness, although there are no comments to discuss it.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
4d
Peak period
75
96-108h
Avg / period
40
Comment distribution160 data points
Loading chart...
Based on 160 loaded comments
Key moments
- 01Story posted
Nov 18, 2025 at 11:23 AM EST
about 2 months ago
Step 01 - 02First comment
Nov 22, 2025 at 4:36 PM EST
4d after posting
Step 02 - 03Peak activity
75 comments in 96-108h
Hottest window of the conversation
Step 03 - 04Latest activity
Nov 24, 2025 at 7:00 AM EST
about 2 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45968310Type: storyLast synced: 11/23/2025, 12:07:04 AM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
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).
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.
https://github.com/tc39/proposal-pattern-matching
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.
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.
I have bad news for you
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.
What’s the point of this level of autism when you still have to add run time checks?
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.
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.
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.
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.
But you can't reason on the different cases aided by the type checker.
Also, using classes or POJOs is merely an implementation detail.
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.
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.
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.
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/
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.
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.
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.
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?
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.
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.
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.
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.
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.
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.
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.
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.
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.
My interpretation of OP's point is that excessive complexity can be a "code smell" on its own. You want to use the solution to match the complexity of the job and both the team that is building it and the one that is likely to maintain it.
As amused as I am by the idea of a dev team being debased by the inelegance of basic bitch programming, the daily reality of the majority of software development in industry is "basic bitch" teams working on "basic bitch" problems. I would argue this is a significant reason why software development roles are so much at risk of being replaced by AI.
To me, it's similar to the choice one has as they improve their vocabulary. Knowing and using more esoteric words might allow adding nuance to ideas, but it also risks excluding others from understanding them or more wastefully can be used as intelligence signalling more than useful communication.
tldr: Complexity is important when it's required, but possibly detrimental when it's not.
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.
Python: list[Any]
...what am I missing?
That was... non-trivial.
Without internet or AI I wouldn't attempt writing anything like that.
The average X dev in Y language doesn't need to understand Z is a poor argument in the context of writing better software.
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.
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...It's a good response to the claim that we'd be surprised at how many would fail to do this, though.
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.In other words, if you flatten `[string, [string, number]]` my example would give you `[string, string, number]` whereas the one in lib.es2019.array.d.ts would give you `(string | number)[]`
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.
If you're purely writing Typescript then you mostly don't need it.
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.
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.
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.
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?
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>?
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.
https://www.google.com/search?q=site%3Ahttps%3A%2F%2Fwww.typ...
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.
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.
Maybe they're smart, but the even smarter dev would avoid unnecessary complexity in the first place.
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.
Frankly, I prefer it that way. A lot of the advance stuff doesn’t actually enable any new functionality. It only shortens the code paths for implementing.
I think you're both exaggerating your blanket accusations of incompetence and confusing learning curve with mastering extremely niche techniques akin to language gotchas.
The language isn’t the bottleneck
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.
You might be interested in reading PG's treatise on "the blub paradox".
Also, Angular dev spotted :)
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.
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).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.
(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.
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.
const x: Thetype = ....
I am not keen on as const either. Just program to interfaces. It is a better way to think IMO.
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.
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.
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.
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.
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.
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.
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'.
```ts
/*
* A function that asserts that a value is never.
* Useful for exhaustiveness checks in switch statements.
*/
export function assertNever(x: never): never {
}```
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?".
https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAllApg...
https://www.typescriptlang.org/play/?#code/DYUwLgBAHgXBB2BXA...
Typescript in a nutshell. That said, satisfies is a good keyword!
> 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).