8 ms·
The problem with the premise of the article is that it presupposes that a library’s interfaces will, and ultimately must, be complex. The great thing about Type
by eyelidlessness 4y ago
The problem with the premise of the article is that it presupposes that a library’s interfaces will, and ultimately must, be complex. The great thing about TypeScript for library development, if you start with types, is that it strongly encourages you to create simpler and less flexible interfaces. That’s not to say you don’t sometimes need (or won’t sometimes choose) to use some of TypeScript’s more complex features. But a lot of the time you won’t, or should think seriously about whether your interfaces are becoming too complex.
Much of the complexity of TypeScript’s type system is a direct result of excessively dynamic interfaces in existing, untyped JavaScript. Starting with types is a good way to catch that early, rethink the interface, and either isolate it or avoid it entirely.
- matsemann 4y agoIt was bad the first few years, with existing code being hard to type because it was creatively written. But now that almost everything is somewhat typed, JS has converged to a way of writing code, and you should consider the tradeoffs before choosing a different path than the current norm.
- fullstackchris 4y agoI really _wish_ this were true, but go look at a vareity of "modern" React projects. React is so flexible that I've seen nightmare implementations of what could be reduced to just a few short lines. Then people turn around and blame React as a 'garbage framework'. Then if you talk about a much more verbose and strict framework like Angular, people turn around and complain "I can't do what I want with this overly opinionated framework!" Damned if you do, damned if you don't. However, if you look behind the curtains of these infamous gripes (some of them so often repeated they are cliché), 99% of the time its people who just haven't spent enough time around a certain framework or language and go create a rant post (similar to OPs post) You're free to love the tools and languages you use all the time. But it's bad practice to go around bad mouthing tools and frameworks you don't full understand or haven't used productively.
- zdragnar 4y agoIt also was missing a lot of crucial features early on, which made typing certain things impossible. Even now there's a few things that are a PITA to type, but as you say those edge cases can be largely avoided.
- acemarke 4y agoI'll agree, and disagree. I made a similar comment in 2019 about "TS pushes you towards simpler APIs" [0], so I agree that it's both a good thing in principle, and something TS usage leans towards in practice. At the same time... JS _is_ a very dynamic language, library design reflects that, and library code by its very nature must account for the different ways that users will want to call it. That causes library types to be much more complex than app types. [0] https://blog.isquaredsoftware.com/2019/11/blogged-answers-learning-and-using-typescript/#typescript-pushes-you-towards-simpler-apis https://blog.isquaredsoftware.com/2019/11/blogged-answers-le...
- bwestergard 4y ago"JS _is_ a very dynamic language, library design reflects that" I would argue it should not. If your API cannot be well expressed in the Typescript type system, it is your API that should change.
- cogman10 4y agoYup. Especially since types are documentation. If you are using some meta dynamic type garbage, then I as a user of the API am left to wonder "Ok, this takes <U, K extends keyof U>... WTF is it expecting?" This isn't to say that sort of thing is ALWAYS wrong, but rather it should be the exception and not the rule. You do types because constraints make code clearer. By having a BF compiler in the middle of your template definition, you've defeated the types and you might as well put an `any` there with docs that say "here there be dragons!"
- hmsimha 4y agoThis is all well and good until you have to deal with other code and other APIs in your library (results from network requests, localstorage, untyped javascript libraries, etc.)
- naet 4y agoI'd rather not warp my whole project around a typing system if I don't need to. I also might not be the owner of every API I need to consume... I think TS is great in certain cases- I love being able to get intellisense info from TS. When I download a new library and I get those helpful hints built right in from the libraries TS adoption, I love it. Typescript in this instance is reducing friction in my development workflow. In my day job I work at an agency where we rapidly deliver a lot of new web properties. I have used Typescript on projects when it was a client requirement, but in our environment using Typescript generally feels like it overall adds friction and time to the project. Some team member inevitably gets stuck spending a bunch of extra time working with TS stuff: setting up the project with proper rules, writing some complex interface to deal with some random API we are either consuming or creating ourselves (in which case it is usually under rapid prototype development and needs to be updated constantly), etc. I'm sure it is very helpful when working at scale with tons of developers on a highly stable and established project, but in my opinion it isn't generally applicable to every web property that it would be better on Typescript; it really depends on the scale of the project and the scale of the team maintaining it.
- refactor_master 4y agoI agree, and it’s the same with Python. If you start with static, type-safe first, then Python’s flexibility ends up being so horribly convoluted that you avoid it in favor of simpler constructs. **kwargs? Massive union types? Super flexible dicts? Rather not! I’ll define 50 strongly typed constructs over having to run my code through 50 times to catch all the runtime bugs.
- rocqua 4y agoI have found *kwargs useful for wrapper functions, where you pass kwargs to some other, later defined, function.
- dfabulich 4y agoHere'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.
- ricardobeat 4y agoAgree 100%. Redux et al are terrible examples where complexity is through the roof, these libraries were written way before TypeScript was the norm. You can eliminate almost all of that by having simpler interfaces, but that requires a different API built with types in mind.
- david422 4y agoWe were trying to add types to some of our code. And the types were getting ridiculously convoluted. And at that point it was like - maybe it's not the types that are the problem, maybe it's the interface that needs to be rewritten.
- emn13 4y agoThe OP's example of createActions smells like such a scenario. The way redux mashes up payloads and discriminators makes it hard to type correctly - as in objects are not simply composed, they're extended by adding properties, and for no immediately obvious reason (which might be because of my inexperience, granted). Had redux been developed in typescript from the start, I doubt it would have chosen this API. Then again, the current version is already in typescript, so perhaps that optimism is unwarranted; unfortunately my experience with it is years ago already.
- acemarke 4y agoThis particular use case is trying to strongly type the "Flux Standard Action" object shape. An object _may_ have a `payload` field for its data, _may_ have an `error` field that indicates this action represents an error, and _may_ have a `meta` field that provides additional descriptive information. This isn't part of the Redux core package, but it's a convention the community adopted shortly after Redux became popular. Redux Toolkit uses that as the standard structure for an action object. `createAction` defaults to just `{type, payload}`, but you can optionally provide a callback that adds the other `meta` and `error` fields. So yes, there's some additional complexity here because of the optional field contents, and this type is telling TS what the final type should look like based on which fields exist. Note that as an end user, you normally don't even call `createAction` yourself - it's automatically called as part of our `createSlice` API. What end users normally write is: export const counterSlice = createSlice({ name: 'counter', // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { increment: state => { state.value += 1 }, // Use the PayloadAction type to declare the contents of `action.payload` incrementByAmount: (state, action: PayloadAction<number>) => { // `action.payload` is now a `number` state.value += action.payload } } })
- dimmke 4y agoYeah, Redux Saga is known for heavily using JS generators and I wonder how much of this is just the relative obscurity of that language feature for him. It's not exactly a straightforward library.
- throwaway0asd 4y agoAgreed! From the article: How do library developers debug their highly dynamic and heavy use of conditional types, overloads? Don’t do that. It dramatically increases compile time and results in a maintenance nightmare. Keep types primitive. In my own code I use type unions of primitives and/or named types. For objects I use interfaces. With that I am able to provides types for about 98% of my code. That left over 2% is generally for extending the global Array type or extending an event.