3 ms·
It's weird to me how scanning the comments all seem to refer to systems with 100k-ish LoC and dozens of contributors. A big chunk of my job is writing node mic
by suzzer99 4y ago
It's weird to me how scanning the comments all seem to refer to systems with 100k-ish LoC and dozens of contributors.
A big chunk of my job is writing node microservices in AWS Lambda. I do everything I can to avoid shared library code, since past experience tells me there be lots of dragons (mainly in when and how to push or pull lib updates to components). I have a very tiny shared lib that I try to never touch and definitely never introduce breaking changes.
Unit tests are a breeze since I never have to cast objects or worry about generics, etc.
Typescript would slow me down so much and add absolutely no benefit. Maybe I'm misinterpreting though and no one is claiming Typescript would benefit here.
We also have some C# lamdbas and I find writing unit tests for those so much more of a pain - since the shared libs have generics and I'm always casting things. But admittedly I don't know all the tricks.
- hither_shores 4y ago> since the shared libs have generics and I'm always casting things. This indicates to me that you're trying to write code that isn't correct (not doesn't work, but rather only works because of implicit couplings between components) and/or doing exotic lisp-style metaprogramming. In the latter case, yeah, C#'s type system isn't powerful enough. Others are (to an extent: arbitrary code execution at compile time is never going to be completely safe). In the former case ... that should be difficult. Forcing you to be explicit is half the point of a type system.
- noduerme 4y agoCasting is often necessary for parsing inbound data from certain mysql libraries or CSV or JSON depending on how it's written. I would guess that might be what the parent is talking about. That said, if you don't cast or parseFloat or whatever in JS you're going to have a lot of trouble. And if you're doing that, why not do it in Typescript where you'll know that the data you're accessing has been safely cast based on its type.
- xigoi 4y ago> Casting is often necessary for parsing inbound data from certain mysql libraries or CSV or JSON depending on how it's written. No, that's what sum types are for.
- noduerme 4y agoI don't see how they're mutually exclusive. I use union types prior to typeguards to end up with ultimately checked, cast values. Say I have a boolean fetched from JSON as "1" or "0". By the time I expose it to the rest of the code as part of a Record, I want to change it to an actual boolean. At first I'm going to treat the inbound value as a union type, e.g. (String | Number | null | undefined | Error). After I deal with the error cases, I'm going to cast it (or in TS, reinitialize it as a single type, boolean) so that any code looking at the imported value sees it as definitely only a boolean without needing to have lots of different pieces of code run their own checks on its type.
- xigoi 4y agoI wouldn't really call that casting, that's just narrowing based on control flow.
- deleted 4y ago[deleted]
- suzzer99 4y agoI mean casting to get mocks to work in unit tests, not live code. Probably there's a better way. I'm not a C# expert. But I was a Java dev for 10 years so it's not super unfamiliar.
- marcosdumay 4y ago> Unit tests are a breeze since I never have to cast objects or worry about generics Generics reduce the amount of things you must care about on your tests. And you shouldn't cast objects in almost no code ever. Most 100k LoC programs won't need it even once, your microservices should need it proportionally less. That's the thing. The gains grow superlineraly with the amount of code. They make it just a bit easier to write some trivial 100's LoC programs, and they make it possible at all to have a working 100k LoC system. But if you don't learn them, you won't know where the break-even point is for you.
- gary17the 4y ago> The gains [introduced by a strong type system] grow superlineraly with the amount of code. That's a good way to put it.
- veidelis 4y ago"And you shouldn't cast objects in almost no code ever." - I have a question about tests. Imagine I want to test a function that operates on quite large application state but not all app state is necessary for that function. Options: - Define all app state as a snapshot. Problem: snapshot can become stale, so more infra might be necessary to make sure that snapshot is up to date; - Pass only the necessary state and construct as necessary. Problem: hard to define whole state precisely and ensure that it conforms to runtime state of a healthy app; - Pass a subset of necessary state for some execution branch and cast the type. Problem: casting may result in test failures during runtime and potentially other issues such as modify-run-fail debug loop; - Mock return values of functions called within the function being tested and use any combination of "state passing options above". In a lot of places I use such approach with custom type helpers and transitive types, and passing in only the necessary subset for smaller functions or mocking return values for bigger ones. What do you think? I know that the AppState can be defined as a union of possible states and together with type guards can address those issues better. I just wanted to hear your opinion on how you would address such problems. I hope I explained it well enough. export type Fn = (...params: any) => any; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; export type FirstParamType<G> = G extends Fn[] ? UnionToIntersection<Parameters<G[number]>[0]> : G extends Fn ? Parameters<G>[0] : never; export interface AppState { first: { a: number; b: number[]; }; second: { c: string; d: string[]; } } type DeepPick<A, B extends keyof A, C extends keyof A[B]> = { [BK in B]: Pick<A[B], C> }; function calculateUsingFirstB(state: DeepPick<AppState, "first", "b">): number[] { return state.first.b; // some calculation } function calculateUsingSecondC(state: DeepPick<AppState, "second", "c">): string { return state.second.c; // another calculation } // function which takes complex state parameter and calculates the result based on results of other functions function calculateMore(state: FirstParamType<[typeof calculateUsingFirstB, typeof calculateUsingSecondC]> & DeepPick<AppState, "first", "a">): string | number[] { if (state.first.a > 10) { return calculateUsingFirstB(state); } return calculateUsingSecondC(state); }
- icedchai 4y agoI've seen plenty of Lambda code developed in a "copy-and-paste" style, with little to no code sharing, similar to early CGI scripts from 25+ years ago. It makes maintainability incredibly difficult. The more shared code the better, in my opinion.
- suzzer99 4y agoYeah if it's really shared code. But if you're just trying to factor out small redundancies that are only implemented a few times and could diverge in the future - then no. I'm a big fan of WET - write everything twice. Then maybe the 3rd of 4th time worry about creating some new abstraction to share code. It's so much easier to add an abstraction later when it becomes obviously needed - than to remove one when you find out your three things actually need different variations on the lib code.