5 ms·
> Hell, ask someone to write a signature for array flat, you'd be surprised how many would fail. To be clear, an array flat type: type FlatArr<Arg extends
by pcthrowaway 11mo ago
> Hell, ask someone to write a signature for array flat, you'd be surprised how many would fail.
To be clear, an array flat type:
type FlatArr<Arg extends unknown[]> = Arg extends [infer First, ...(infer Rest)] ?
First extends unknown[] ?
[...First, ...FlatArr<Rest>] :
[First, ...FlatArr<Rest>] :
[];
is far from basic Typescript. The average Typescript dev likely doesn't need to understand recursive conditional types. It's a level of typescript one typically only needs for library development.
Not only have I never been expected to write something like this for actual work, I'm not sure it's been useful when I have, since most of my colleagues consider something like this nerd sniping and avoid touching/using such utilities, even with documentation.
- wk_end 11mo agoIf I saw that in a PR I would push very hard to reject; something like that is a maintenance burden that probably isn’t worth the cost, and I’ve been the most hardcore about types and TypeScript of anyone of any team I’ve been on in the past decade or so. Now, that said, I probably would want to be friends with that dev. Unless they had an AI generate it, in which case the sin is doubled.
- deleted 11mo ago[deleted]
- 8note 11mo agolooking back at them is also real hard to debug. you dont get a particularly nice error message, and a comment or a test would tell better than the type what the thing should be looking like
- probabletrain 11mo agoI think there’s a difference between what’s expected/acceptable for library code vs application code. Types like this might be hard to understand, but they create very pleasant APIs for library consumers. I’ve generally found it very rare that I’ve felt the need to reach for more complex types like this in application code, however. RXJS’s pipe function has a pretty complex type for its signature, but as a user of the library it ‘just works’ in exactly the type-safe way I’d expect, without me having to understand the complexity of the type.
- spankalee 11mo agoWhat's the alternative? Have incorrect types for the function? That's not better.
- epolanski 11mo agoThe alternative is what shows in the comment: go on HN and tell the world you think TS and JS are crap and it's not worth your time, while writing poor software.
- wk_end 11mo agoTo answer this we probably need more details, otherwise it's gonna be an XY Problem. What is it that I'm trying to do? How would I type this function in, say, SML, which isn't going to allow incorrect types but also doesn't allow these kinds of type gymnastics?
- spankalee 11mo agoWe don't have to deal in hypotheticals - we have a concrete example here. There's a method, array.flat() that does a thing that we can correctly describe in TypeScript's type system. You say you would reject those correct types, but for what alternative? It's hugely beneficial to library users to automatically get correctly type return values from functions without having to do error-prone casts. I would always take on the burden of correct types on the library side to improve the dev experience and reduce the risk of bugs on the library-consumption side.
- wk_end 11mo agoThere's nothing I can do about the standard JavaScript library, but in terms of code I have influence over, I very simply would not write a difficult-to-type method like Array.prototype.flat(), if I could help it. That's what I mean by an XY Problem - why are we writing this difficult-to-type method in the first place and what can we do instead? Let's suppose Array.prototype.flat() wasn't in the standard library, which is why I'm reviewing a PR with this gnarly type in it. If I went and asked you why you needed this, I guess you'd say the answer is: "because JavaScript lets me make heterogenous arrays, which lets me freely intermix elements and arrays and arrays of arrays and... in my arrays, and I'm doing that for something tree-like but also need to get an array of each element in the structure". To which I'd say something like "stop doing that, this isn't Lisp, define an actual data type for these things". Suddenly this typing problem goes away, because the type of your "flatten" method is just "MyStructure -> [MyElements]".
- ibejoeb 11mo agoIf it's correct, it's not a maintenance nightmare, and it will alert you to problems later when someone wants to use it incorrectly. If you're writing first-party software, it probably doesn't matter. But if you have consumers, it's important. The compiler will tell you what's wrong all downstream from there unless someone explicitly works around it. That's the one you want to reject.
- locknitpicker 11mo ago> If it's correct, it's not a maintenance nightmare, and it will alert you to problems later when someone wants to use it incorrectly. You're confusing things. It is a maintenance nightmare because it is your job to ensure it is correct and remains correct in spite of changes. You are the one owning that mess and held accountable for it. > If you're writing first-party software, it probably doesn't matter. But if you have consumers, it's important. Yes, it is important that you write correct and usable code. That code doesn't fall on your lap though and you need to be the one writing and maintaining it. Whoever feels compelled to write unintelligible character soup that makes even experienced seasoned devs pause and focus is failing their job as a software engineer.
- ibejoeb 11mo ago> Whoever feels compelled to write unintelligible character soup... I see it differently. That's the name of the game. Language design is always striving toward making it more intelligible, but it is reasonable to expect pros to have command of the language.
- locknitpicker 11mo ago> I see it differently. That's the name of the game. Language design is always striving toward making it more intelligible, but it is reasonable to expect pros to have command of the language. No, that's an extremely naive and clueless opinion to have. Any basic book on software engineering will tell you in many, many ways that the goal of any software engineer is to write simple code that is trivial to parse, understand, and maintain, and writing arcane and overly complex code is the Hallmark of an incompetent developer. The goal of a software engineer is to continuously fight complexity and keep things as simple as they can be. Just because someone can write cryptic, unintelligible code that doesn't make them smart or clever: it only makes them bad at their job.
- pjerem 11mo agoI’d say it depends. I always advocate for code that is easy to read and to understand, but in extremely rare conditions, hard to read code is the better solution. Especially when it comes to signatures in Typescript, complex signatures can be used to create simple and ergonomic APIs. But anyway you shouldn’t be allowed to push anything like this without multiple lines of comments documenting the thing. Unreadable code can be balanced with good documentation but I rarely saw this unfortunately.
- chuckadams 11mo agoComments and tests. Vitest supports unit tests for types.
- swiftcoder 11mo ago> If I saw that in a PR I would push very hard to reject; something like that is a maintenance burden that probably isn’t worth the cost As someone who came from a CS background, this kind of attitude is deeply mysterious. That seems like a type expression I'd expect a CS undergrad to be able to write - certainly if an SDE with 1-2 years experience was confused by it, I'd be advocating against their further promotion.
- shortrounddev2 11mo agoThe every day practice of software engineering has little to do with the academic discipline of computer science. What makes a good software engineer is not usually the same thing that makes a good CS major
- plaguuuuuu 11mo agobut then you wind up with an entire repo, or an entire engineering team utterly hobbled by a lack of expressive typing (or advanced concepts generally) and debased by the inelegance of basic bitch programming.
- FlyingAvatar 11mo agoDisclaimer: I'm not the OP, and there are certainly places where using recursive type definitions is justified. My interpretation of OP's point is that excessive complexity can be a "code smell" on its own. You want to use the solution to match the complexity of the job and both the team that is building it and the one that is likely to maintain it. As amused as I am by the idea of a dev team being debased by the inelegance of basic bitch programming, the daily reality of the majority of software development in industry is "basic bitch" teams working on "basic bitch" problems. I would argue this is a significant reason why software development roles are so much at risk of being replaced by AI. To me, it's similar to the choice one has as they improve their vocabulary. Knowing and using more esoteric words might allow adding nuance to ideas, but it also risks excluding others from understanding them or more wastefully can be used as intelligence signalling more than useful communication. tldr: Complexity is important when it's required, but possibly detrimental when it's not.
- kaoD 11mo agoFor those unfamiliar with TS, the above is just... function flat([head, ...tail]) { return Array.isArray(head) ? [...flat(head), ...flat(tail)] : [head, ...flat(tail)] } ...in TS syntax.
- tomsmeding 11mo agoWell, it is the type of that, in TS syntax. Few are the statically-typed languages that can even express that type.
- not_kurt_godel 11mo agoJava: List<Object> Python: list[Any] ...what am I missing?
- sli 11mo agoYou're missing the input type, essentially. Those are just array types. The TypeScript type signature more of a function type, it expresses flattening a n-dimensional array (input type) into a flat array (output type).
- rjh29 11mo agoYou're missing the specialisation of Object/Any. For example Array.flat called with [int, [bool, string]] returns a type [int, bool, string]. Admittedly this is somewhat niche, but most other languages can't express this - the type information gets erased.
- miki123211 11mo agoI recently had to write a Promise.all, but using an object instead of an array. That was... non-trivial.
- hdjrudni 11mo agoIf it's what I'm thinking, that one isn't too bad. I wrote it awhile back: export async function promiseAll<T extends Record<string, Promise<any>>>(promises: T): Promise<{ [K in keyof T]: Awaited<T[K]> }> { const keys = Object.keys(promises) as Array<keyof T>; const result = await Promise.all(keys.map(key => promises[key])); return Object.fromEntries(result.map((value, i) => [keys[i], value])) as { [K in keyof T]: Awaited<T[K]> };
- atsjie 11mo agoI'd call that bad pretty bad. Without internet or AI I wouldn't attempt writing anything like that.
- ruined 11mo agorejoice https://github.com/tc39/proposal-await-dictionary https://github.com/tc39/proposal-await-dictionary
- epolanski 11mo agoThe version I was thinking when I wrote the comment is simpler type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T > The average Typescript dev likely doesn't need to understand recursive conditional types. The average X dev in Y language doesn't need to understand Z is a poor argument in the context of writing better software.
- NooneAtAll3 11mo agoas a person that never touched JS and TS... what's the difference between the two answers?
- ameliaquining 11mo agoFirst one flattens a potentially-nested tuple type. E.g., FlatArr<[number, [boolean, string]]> is [number, boolean, string]. Second one gets the element type of a potentially-nested array type. E.g., Flatten<number[][]> is number. For what it's worth, I've never needed to use either of these, though I've occasionally had other uses for slightly fancy TypeScript type magic.
- granzymes 11mo agoFor one, the simple answer is incomplete. It gives the fully unwrapped type of the array but you still need something like type FlatArray<T extends unknown[]> = Flatten<T[number]>[] The main difference is that the first, rest logic in the complex version lets you maintain information TypeScript has about the length/positional types of the array. After flattening a 3-tuple of a number, boolean, and string array TypeScript can remember that the first index is a number, the second index is a boolean, and the remaining indices are strings. The second version of the type will give each index the type number | boolean | string.
- pcthrowaway 11mo agoThe answer above actually gets the type union of all non-array elements of a multi-level array. In other words Flatten<[1,[2,'a',['b']]]> will give you a union type of 1, 2, 'a', and 'b' const foo: Flatten<[1,[2,'a',['b']]]> = 'b'; // OK const bar: Flatten<[1,[2,'a',['b']]]> = 'c'; // Error: Type '"c"' is not assignable to type '1 | 2 | "a" | "b"' Technically the inference is unnecessary there, if that's you're goal: type Flatten<T> = T extends Array<unknown> ? Flatten<T[number]> : T I don't really consider this the type of flattening an array, but `Array<Flatten<ArrType>>` would be. And this would actually be comparable to the builtin Array.prototype.flat type signature with infinite depth (you can see the typedef for that here[1], but this is the highest level of typescript sorcery) My solution was for flattening an array with a depth of 1 (most people using Array.prototype.flat are using this default depth I'd wager): console.log(JSON.stringify([1,[2, [3]]].flat())); > [1,2,[3]] The type I provided would match those semantics: // 'readonly' added to the 'extends' sections to work on "as const" (readonly) arrays type FlatArr<Arg extends unknown[] | readonly unknown[]> = Arg extends readonly [infer First, ...(infer Rest)] ? First extends readonly unknown[] ? [...First, ...FlatArr<Rest>] : [First, ...FlatArr<Rest>] : []; const flatten = <Arr extends unknown[] | readonly unknown[]>(arr: Arr): FlatArr<Arr> => arr.flat() as FlatArr<Arr> const someArr = [1,[2,'a',['b', ['c']]]] as const; // const someArr: readonly [1, readonly [2, "a", readonly ["b", readonly ["c"]]]] const someArr2 = flatten(someArr); // const someArr2: [1, 2, "a", readonly ["b", readonly ["c"]]] [1]: https://github.com/microsoft/TypeScript/blob/main/src/lib/es2019.array.d.ts https://github.com/microsoft/TypeScript/blob/main/src/lib/es...
- chrismorgan 11mo agoHere’s the fun part that I suspect many here are forgetting: if you want to write the function body, it will probably (or at the very least can) look very similar! function flat() { // type FlatArr<Arg extends unknown[]> = let [first, ...rest] = this; return (this.length > 0) ? // Arg extends [infer First, ...(infer Rest)] ? Array.isArray(first) ? // First extends unknown[] ? [...first, ...flat(rest)] : // [...First, ...FlatArr<Rest>] : [first, ...flat(rest)] : // [First, ...FlatArr<Rest>] : []; // []; } If you want to unternarise it: function flat() { if (this.length > 0) { let [first, ...rest] = this; if (Array.isArray(first)) { return [...first, ...flat(rest)]; } else { return [first, ...flat(rest)]; } } else { return []; } } I still wouldn’t call it basic TypeScript, but it’s not conceptually that advanced, you just need to know about infer and extends. Now in reality, Array.prototype.flat has a more complex definition, partly because (like most of Array’s methods) the method is generic (it works on array-like objects that have a length property and numeric indexing), and partly because of the depth parameter. From lib.es2019.array.d.ts: type FlatArray<Arr, Depth extends number> = { done: Arr; recur: Arr extends ReadonlyArray<infer InnerArr> ? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]> : Arr; }[Depth extends -1 ? "done" : "recur"]; interface ReadonlyArray<T> { // [Snipped: flatMap, and flat’s doc comment] flat<A, D extends number = 1>( this: A, depth?: D, ): FlatArray<A, D>[]; } // [And repeated for interface Array<T>.] Ouch. Don’t like the { done, recur }[Depth extends -1 ? "done" : "recur"] at all, no idea why it wasn’t written as `Depth extends -1 ? Arr : Arr extends ReadonlyArray<…`. And as for hard-coding support for depths up to 20 then bailing… probably pragmatic, it’s possible to support all values, but rather messy: https://stackoverflow.com/q/54243431 https://stackoverflow.com/q/54243431.
- pcthrowaway 11mo agoNotably, the provided type for Array.prototype.flat doesn't actually provide the flat type for arrays of a known structure. In other words, if you flatten `[string, [string, number]]` my example would give you `[string, string, number]` whereas the one in lib.es2019.array.d.ts would give you `(string | number)[]` (my example, on the other hand, would only flatten a depth of 1, though a completely flattened type instead of just depth 1 could be represented by changing `[...First, ...FlatArr<Rest>] :` to `[...FlatArr<First>, ...FlatArr<Rest>] :` I haven't tried supporting a depth param, but I suspect it's possible.