4 ms·
Here's a perfect example. I maintain a simple Node library designed to connect to Apple's App Store Connect API. https://github.com/dfabulich/node-app-store-con
by dfabulich 4y ago
Here's a perfect example. I maintain a simple Node library designed to connect to Apple's App Store Connect API. https://github.com/dfabulich/node-app-store-connect-api https://github.com/dfabulich/node-app-store-connect-api
It accepts, as a parameter, a URL for Apple's REST API. My library handles authentication, and returns the parsed JSON result, with a handful of tweaks to make the API more usable in JavaScript.
Depending on which URL you request, you'll get different result object back. You could get a single object in response, or an array of objects, and the type of returned objects is different for each URL type.
How would you add TypeScript types to this API? Well, Apple provides an OpenAPI documentation of all of their URLs, which I could use to autogenerate types, but then, how would I handle all of those types in response to the user's string input?
Well, it turns out that TypeScript is so amazingly fancy that you can write very clever code to parse strings at compile time, extracting parameter types etc. from string literal types. https://lihautan.com/extract-parameters-type-from-string-literal-types-with-typescript/ https://lihautan.com/extract-parameters-type-from-string-lit...
The documentation explains how an API like this:
app.get('/purchase/[shopid]/[itemid]/args/[...args]')
can parse its parameters into a Request type with shopid, itemid, and args[] array parameters. This would catch a bug if you had a typo, e.g. "itmid".
But the code to do that looks like this:
type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never;
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}`
? IsParameter<PartA> | FilteredParts<PartB>
: IsParameter<Path>;
type ParamValue<Key> = Key extends `...${infer Anything}` ? string[] : number;
type RemovePrefixDots<Key> = Key extends `...${infer Name}` ? Name : Key;
type Params<Path> = {
[Key in FilteredParts<Path> as RemovePrefixDots<Key>]: ParamValue<Key>;
};
type CallbackFn<Path> = (req: { params: Params<Path> }) => void;
function get<Path extends string>(path: Path, callback: CallbackFn<Path>) {
// TODO: implement
}
Nifty, eh? But, as the article says: how would you test this code? How would you debug it?
Clearly, I wouldn't do that. Instead, I'd write a script to autogenerate individual methods, e.g. instead of get(`apps/${appId}`) that returns a parsed JSON blob, I'd autogenerate a getApp(appId) method that returns an App object.
But that API isn't any simpler than the API I already have; it's just different.
And let's not forget that I'd have to write a script to autogenerate these methods (or just their types) based on Apple's OpenAPI specification, and now I have to maintain that code, updating my @types/node-app-store-connect-api definition every time Apple introduces a new URL you can request. Testing and debugging that is a challenge in its own right.
And even if I did it, the complexity of my library just went from a few hundred lines of glue code to 1,000+ lines of type generation (plus tests for the generated types).
This is in no way worth it for me. As a library developer, adding TypeScript types would make my life harder.
- biorach 4y ago> And even if I did it, the complexity of my library just went from a few hundred lines of glue code to 1,000+ lines of type generation (plus tests for the generated types). I would say that the increased complexity of your project would accurately reflect the complexity of the underlying API > As a library developer, adding TypeScript types would make my life harder. Yes, but it would mean that the various parameters passed to your library would now be type safe, which sounds worth the work
- dfabulich 4y agoOne clear moral of this story is that, as a library developer, I need a lot more documentation and tooling support from the TypeScript team. That string-parsing thing isn't in TypeScript's documentation at all; it's a random blog post. I'm lucky I found it. TypeScript's site should document how to do this. Furthermore, it should be easier to debug parsed stringly-typed APIs using the `infer` keyword. Today, I just run the compiler over and over again until it stops throwing errors, and the errors are really not very helpful. There should be a standard tool to help me test that I used `infer` correctly, ideally helping me by fuzzing the code to see if I allowed something I shouldn't have allowed, or blocked something that I should have allowed, and to test against various versions of TypeScript. Narrowing types shouldn't be done just by nesting ternary expressions. As the article notes, "It's pretty common in style guides to never nest ternaries. In typescript, that's the only way to narrow types based on other types. It's a mess!" Lastly, this idea doesn't sit right with me: > I would say that the increased complexity of your project would accurately reflect the complexity of the underlying API I designed my library so it doesn't need to incorporate that complexity. Users of my API have to learn the API, but they can learn it from Apple, by reading Apple's documentation, and by inspecting the objects that Apple actually returns (rather than what their OpenAPI specification says they'll return). Incorporating that complexity into my library will make my library harder to work with, more difficult for others to contribute to my library. As the article says, "Types make it much harder to maintain a js library, and especially difficult to contribute to them." TypeScript is forcing me to duplicate the API's complexity in my code. Even if you think that "sounds worth the work," I think you have to agree that the work load is much higher than the work I've already done.