1. 5

  2. 1

    Speaking of abusing TypeScript assertions functions, if anyone has an idea how to make this work I’d be very interested ?

    1. 1

      I see someone commented on the Gist with a way of making it sort of work, and you might be able to extend that using keyof on the resulting mapped type (which lets you exclude keys whose values are never).

      As to why that doesn’t work, though, which I think is equally illuminating: asserts does something interesting (though reasonable, as I’ll cover) there, which you can see if you change the assertion on remove to this:

        remove<Key extends string>(
          key: Key
        ): asserts this is "A potato";

      If you then check the resolved type of store after calling store.remove("foo");, you’ll see that the type is now the previous intersection type & "A potato". It appears (I haven’t worked this through extensively or checked the compiler implementation, just from what I see playing around with it) that asserts <value> is <Type> simply intersects the type of <value> with <Type>. For example, if you write this (playground:

      declare function narrowsToString(x: string | number);
      let x = 123;
      x; // type is `never`, as you would expect from `string & number`

      In the case of your remove call, that intersection behavior reduces to the same thing as was there previously, since T & Omit<T, SomeKey> is just T. (See here for how that works out.)

      In terms of why that’s how the asserts operator works, I would guess it’s because it’s a relatively natural implementation for the desired behavior: it gives exactly the output you would expect for “normal” narrowing flows (including those like the narrowsToString example above; see this extended example)—at least, if you use to actually assert things if the condition doesn’t hold!

      1. 1

        you’ll see that the type is now the previous intersection type & "A potato"

        Yes, that’s what I meant with my comment “Every asserts stays true.” at the top. As you said, this is useful behavior in general for type narrowing :)

        Thank you for putting in work for a random internet comment ^^

        1. 1

          Once I saw the example I couldn’t not work through the exact mechanics of why it works the way it does. 😂

    2. 1

      Properly decoding types with a library like io-ts the way decode unknown types without “misusing” anything. You don’t even have to buy into fp-ts or effect-ts, even if these are good libraries, to use io-ts.

      1. 1

        That’s absolutely correct. And those simply represent special cases of TS’s type narrowing and type guards capabilities, which work much better than the hack I showed here. As I noted in both the introduction and the conclusion to the piece, this is a workaround you would only ever reach for in very specific circumstances where you have an API which already has a specific shape and that cannot be changed for whatever reason.