38 comments

[ 10.2 ms ] story [ 117 ms ] thread
> 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!

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

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

"There basics," well understood and judiciously applied, is where the bulk of TypeScript's value lies.
> 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.

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.
I stopped doing anything advance because I realized nobody actually wanted to do all of the advanced stuff. None of the NPM community does anything more than the basics.

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.

the cool thing about Typescript is that you never have to know any of this to deliver highly performant enterprise scale software
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 :)

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.

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.

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.

The second example confuses me. The Person type has isCool: boolean, not an explicit true. How does using satisfies here pass coolPeopleOnly?
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.

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.

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.
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.
This thread is weird, also spot on, not sure why its downvoted. I am happy when the IDE spots my dufusness.
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.
So satisfies prevent you from mutating then? Otherwise you could just change name afterwards...
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).

99% of my use of `satisfies` is to type-check exhaustivity in `switch` statements:

    type Foo = 'foo' | 'bar';
    
    const myFoo: Foo = 'foo';
    switch (myFoo) {
      case 'foo':
        // do stuff
        break;
      default:
        myFoo satisfies never; // Error here because 'bar' not handled
    }
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

This is what I do:

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

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

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?

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.

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.

Meanwhile, Python can sometimes verify if something is an int or a string.
The contrived example used in the first half of the post is offputting. Why would you have a function of 1 argument that only accepts a string with a specific value? There's no point taking an argument then - it might as well be a constant inside the function.
Lots of people seem to love typescript, but as far as I can tell it's adding features from c++ which lots of people seem to hate. Am I missing something?