3 ms·
Misusing TypeScript assertion functions for fun and profit
- dgritsko 4y agoI'm still learning TypeScript and didn't know about "assertion signatures" prior to reading this article. They were added in 3.7. For any other TS users who also aren't familiar: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions https://www.typescriptlang.org/docs/handbook/release-notes/t...
- tomduncalf 4y agoGood write-up and it is quite a neat feature. We wanted to use it in the library I work on to model how a method of our class mutates an argument to add new properties to it (IIRC, it was a little while ago), but hit an issue that you need to explicitly type the instance of the class for this to be usable – that is, a user can't write "const assertingInstance = new AssertingClass()" but instead has to write "const explicitAssertingInstance: AssertingClass = new AssertingClass()". We decided that this was going to be too inconvenient for our users, and so pursued a different API direction. I raised an issue and Ryan from the TS team gave a good explanation of why this is so, and why it's unlikely to change in the near future (performance penalty of doing the analysis required), but I thought I'd mention it in case anyone is looking to use this feature and might hit the same issues. The TS issue is https://github.com/microsoft/TypeScript/issues/47945 https://github.com/microsoft/TypeScript/issues/47945 if you'd like to see more details.
- eyelidlessness 4y agoI don’t see how this is an abuse of assertions, it’s just unnecessary mutation and the caveats reveal that. For the specific case of adding a known field, you can return a value with that field added. If you create a new value derived from the old one, you also regain a stable object shape (at the expense of allocation). For the general case of extend, all of the same applies. To avoid `never`, you can use Omit or perhaps Exclude to handle overwritten field types. Applying all of that to this simple extend example: function extend<T extends object, U extends object>( value: T, extension: U ): Omit<T, keyof U> & U { return Object.assign(value, extension); } The resulting type may be unwieldy, but you can tame it with a Merge type: type Merge<T> = { [K in keyof T]: T[K]; } // then change the return type to: // Merge<Omit<T, keyof U> & U>
- chriskrycho 4y ago> it’s just unnecessary mutation Yes! The article says as much in both the introduction and the caveats at the end! The only reason to reach for this is: cases where for some reason you cannot change the existing API. As for the `extend` type: you’re right that can make it "work" that way by stomping the existing field, which matches the actual behavior of the thing at runtime, but it also requires you to write it as an assignment to get that behavior, which necessarily perturbs any existing uses of the code. Instead of this: extend(prev, { someValue }); You have to write this: let result = extend(prev, { someValue }); The whole point here is to avoid doing that in the case of an API which you need to represent and cannot require consumers to change. That’s often the undesirable reality that library maintainers find themselves in, especially when converting an old JavaScript library to TypeScript.
- eyelidlessness 4y ago> Yes! The article says as much in both the introduction and the caveats at the end! My point in saying so in that statement wasn’t to focus on the unnecessary mutation! It was to focus on the perfectly legitimate use case for assertion types in attempting to address it. All of the rest is a fair point, and I probably did lean too hard on the mutation point. My intent in taking it further wasn’t to belabor that point, but to show that the merge type is solvable. In hindsight I should have kept the assertion signature and focused on the solution to `never` (and the resulting unwieldy type), because I think that could be genuinely useful for many people. Also in hindsight, I should have taken the time to say that I have attempted the exact same solution to the same problem for a different stateful API (in my case, Node vm.*Module initialization states), and reached the same conclusions. My intent wasn’t to criticize. Nor to dispute anything in the article other than doubting the legitimacy of the approach. Apologies for failing to communicate that effectively.
- chriskrycho 4y agoThanks for the clarification!