TypeScripting the technical interview(richard-towers.com) |
TypeScripting the technical interview(richard-towers.com) |
https://www.typescriptlang.org/play?#code/PTAEEkDsAsEMBsCmAa...
I didn't follow any of that shit. The fuck is an 'n-queen'? I don't think I learned that in my coding bootcamp.
Is that a bad sign?
Ever heard about word “abstraction”?
...or at least not a member of that particular cult.
Literally in the parent post.
From TFA. Even the article struggles to explain how this is useful outside similar leetcode questions. Yes, backtracking is sometimes a useful approach for some real-life programming problems but leetcode questions neither test for the ability to recognize backtracking problems nor the general ability to solve them.
The second is a explanatory story, or "discovery fiction" as the article classifies itself: https://paulbutler.org/2022/what-does-it-mean-to-listen-on-a...
I love these humorous yet pedagogic technical writings, woven with a bit of literary eloquence and down-to-earth narrative. Thank you for this.
https://aphyr.com/posts/353-rewriting-the-technical-intervie...
https://aphyr.com/posts/342-typing-the-technical-interview
(Also linked I the first paragraph of the link you posted, as well as in the intro of the OP.)
The ending is perfect.
Vidrun, born of the sea-wind through the spruce
Vidrun, green-tinged offshoot of my bough, joy and burden of my life
Vidrun, fierce and clever, may our clan’s wisdom be yours:
Never read Hacker News
But Hacker News has read of you, in their snicker-slithing susurrential warrens, and word has spread...That post was in Haskell, where it's not too surprising that you can do serious computation inside the type system.
This new post translates the ideas to TypeScript, which is more widely known, and which I once heard described as having "accidentally Turing-complete" types:
import { Query } from "@codemix/ts-sql";
const db = {
things: [
{ id: 1, name: "a", active: true },
{ id: 2, name: "b", active: false },
{ id: 3, name: "c", active: true },
],
} as const;
type ActiveThings = Query<
"SELECT id, name AS nom FROM things WHERE active = true",
typeof db
>;
// ActiveThings is now equal to the following type:
type Expected = [{ id: 1; nom: "a" }, { id: 3; nom: "c" }];Still don’t understand anything though.
That is pretty neat, and silly.
I also think it highlights my natural aversion to static type checking in dynamic languages. I know that I could get sucked into writing a bunch of code for the checker, instead of using my energy for making the application work.
Ideally, I would have written:
type Nil = unique symbol
Which would ensure the Nil type wouldn't match with anything but itself. Unfortunately, the compiler doesn't allow unique symbol other than on const variables initialised with Symbol(). So I needed some meaningless symbols.I could also have done
type Nil = "nil"
But then it would have looked like the string meant something.“Making the application work” is only half the story. Making a large application stable and maintainable is only really possible with static type safety, IMHO.
Personally I also find coming up with the correct types to be gratifying if they’re a little tricky.
But in a smaller shop, oncall are developers, that is, ourselves.
Template metaprogramming is all about code generation. You would expect them to look different.
I’ve never worked in a Typescript shop, is there any truth to the satire here? The sea of confusing types to solve any problem?
I'm reminded of https://github.com/type-challenges/type-challenges -- I've only looked at some of the more challenging problems, but one involves writing a JSON parser in the type system. The easy problems look reasonably useful to solve.
There's a recursion depth limit of 500 on the TypeScript compiler, which prevents this solution working for N > 7
Even Aphyr's original Haskell solution only demonstrates N = 6, so in some sense this is an improvement on the state of the art for type-level N Queens solutions /s
> Invoke the compiler
$ tsc *.ts --lib esnext --outFile /dev/stdout
var ᚾ = Symbol();
var ᛊ = Symbol();
var ᛚ = Symbol();
var ᛞ = Symbol();
Why is there so little output? Is that because the compiler removed the unneeded types? And the author is implying that the task was boilerplate and pointless?I would not hire nor want to work with this developer.
I was going to try to pick out one of my favourites from this series, but I really can't. Every last one is a treasure.
EDIT: Oops! This is based on Aphyr's work. My bad!
This post is a pastiche of https://aphyr.com/posts/342-typing-the-technical-interview
This post translates one of them from Haskell to typescript (very well IMO).
I wish TS didn't evolve into this complexity. Library types that should be simple turn into a machine you have to understand (for no good reason).
There's a bug in the Solve "function" due to which you'll get the right answer only for 1x1, 5x5 and 7x7 (I checked till 8x8).
The base case makes a wrong assumption that there will always be a candidate available for the last row. If there are no candidates available, it should return Nil, and backtrack.
Basically, replace
Concat<candidates, placedQueens>
with candidates extends Cons<infer x, any>
? Cons<x, placedQueens>
: NilI've corrected this, and credited you in the errata - https://www.richard-towers.com/2023/03/11/typescripting-the-...
Even so, JS itself being so dynamic, TS still can’t claim full type safety.
And as much as people complain about the type system’s verbosity, many newer features are designed specifically to allow you to be much more terse while improving expressivity and safety. A great example: the satisfies operator lets you narrow a value’s type to conform to whatever it satisfies, and simultaneously widen it to whatever it adds (including anything optional in the narrower type). This is great for composition, only takes two words to accomplish. And its meaning should be immediately obvious at a glance once you know about the operator.
which tends to happen at the edge of the application where it interacts with the outside world and all the interesting things happen.
The part about using the typescript language server to compute the solution, and the protagonist claiming the code is "concise" because only 4 lines of javascript were generated, was absolutely brilliant. Cracked me up at least.
Glancing at the actual code, I admit I'm with Criss in my ability to follow the logic, but it doesn't look like a direct translation from Haskel types to Typescript types either.
At any rate, very well done.
0: https://github.com/microsoft/TypeScript/issues/14833
1: https://github.com/microsoft/TypeScript/issues/14833#issueco...
Where complex types can be a problem is when working with open source libraries, especially when the types are community-developed, separate to the library itself. The library API may not be particularly amenable to easy typing, and the community types can end up being rather confusing, especially to people who developed neither the types nor the original library.
In my experience, 90% of the time when a developer uses any, they just don't know about unknown. 9% it's because they are lazy. 1% is because you are implementing something from an imported library, and they fell into the other 99%.
In this case it's using the type system to calculate the answer to a problem, which is not useful because the type system can't output anything to the console or do other IO. The "answer" will only be visible in your IDE.
The type system is powerful and extremely capable because it had to support existing javascript patterns, like "this function takes a parameter that might be a string or might be a number or might be an array" and make them type-safe.
Mostly in typings either provided by the library itself or via the 3rd party DefinitelyTyped project. Some typings have been made so complex, that it is hard to follow what kind of concrete type is exactly expected or allowed.
As long as you're satisfied with the answer being shown on a tooltip when you hover over a variable... sure.
Notice how the whole type structure ending up as 4 lines of inconsequential javascript after going through the typescript compiler at the very end.
If it's the former, go for it.
If it's the latter, don't worry about it. I have a CS degree, did 6 years on a team writing C++ at Microsoft, and only briefly flirted with understanding this stuff. A couple of the wizard devs on the (very large) team could do it, but they also knew that code needed to be simple above all else, so it was effectively banned in our code base.
The other is building up basic "data types" by pretty standard lambda calculus > LISP route. "Understanding Computation" book has a great chapter 6 on that, which is available in blog & video forms on https://computationbook.com/extras - Here, Church numerals were used to represent numbers. - booleans & conditionals here didn't resort to the lambda representation you'll see in the book, but relied on type conditionals builtin to TypeScript. - The names "Cons" & "nil" are a ringer for LISP-like building of lists, and recursive processing of lists, from a "pair" data type.
What's common to both approaches to building arithmetic is starting from zero + a "successor" function T. That approach is called "Peano arithmetic".
I still recommend that post/video (and the book in general) but I have to admit there is no 1:1 correspondence to the TypeScript going on here.
Still, it'll teach you some general maneuvers for bootstrapping computation out of almost nothing , qnd once you're comfortable with those, you can read things like this TypeScript post, or aphyr's original Haskell post, which bootstrap computation out of sjighly different" almost nothings" and without following the details still have a high-level idea of where it's going (like the poor interviewer in the story ;-)
(I still do it now because I inherited a huge highly dynamic set of projects, and just explaining the portion of the universe I remember at any given time is an exhausting majority of all of my weekly meetings.)
I considered forking the compiler to set a deeper limit, but at some point Typescript itself is going to stack overflow. Also that probably goes a bit beyond what Criss is expecting in an interview...
Also, being able to recognize the Peano S-function on sight (and thus uses of Church-Peano numbers) is a fun part of that wizardry. (Maybe not necessary to remind the usefulness of the Lambda Calculus canon on a site named after one of the most famous combinators.)
I have seen the Typescript "computing this type lead to what appeared to be infinite recursion and I gave up" errors. The compiler is well built, it mostly does not truly crash in an exceptional case like that, but it does have to use hard, practical heuristics to avoid getting stuck in infinite loops (including harsh stack depths and calculation timeouts for type checking).
This doesn't mean type systems are useless! But as GP said there are tradeoffs, and suspiciously many language designers gave up "clean" properties like guaranteed-terminating compiler, or you know actually guaranteeing run-time safety...
BTW, TypeScript documents where it's unsound and why consciously made those choices: https://www.typescriptlang.org/docs/handbook/type-compatibil...
“Oh, right.” He blusters a non-termination argument at nobody in particular. “That’s why we usually think in subsets of Haskell where types don’t have bottom values.”
const foo = <T extends Record<string, any>>(dict: T) => …
This is a good signal that foo maps over dict in some generic way that cares more about its dictionary-ness than its values. Sure, unknown works in that position too, but at least IMO the “doesn’t care” bit is more informative than “doesn’t know”. The latter might imply more type narrowing will happen than is the case.Use Object.defineProperties and TS complains because that stuff is invisible to it after how many years?
I think you're right, of course, but TS is hardly perfect and treating its ways as gospel is not an improvement over JS. The "right ways" change over time and beliefs are not shared among everyone.
TS is far from perfect. These aren't its ways (it provides any, so of course it's fine with it). These are my restrictions: if you're using a type system, actually use it. Don't lie to yourself and throw anys in your code.
How do you get around properties assigned via Object.defineProperties recognised by TS, though? I really don't know. Is is an unrelated question
interface IOptionsX {
x: number;
}
interface IOptionsY {
y: number;
}
const testObj: IOptionsX = {x : 0};
Object.defineProperties(testObj, {
y: {
value: 100,
writable: true
},
});
if ('y' in testObj) { // Could also do `&& typeof testObj.y === 'number'` to be VERY sure.
const testObjWithY = testObj as typeof testObj & IOptionsY;
console.log(testObjWithY.y)
}So now you've gone from not caring to "enabling someone to shoot themselves in the foot" if they don't read the types of the parameters carefully. That's the difference.
That's what `unknown` is for. `any` behaves like `never` (in covariant positions), which is exactly the opposite.
And in any case, I’d almost always lean towards the option with stronger type safety guarantees. Especially in a team environment when someone else may be modifying your code later. As a convention, I almost never use any.
Also, please don’t continue to be a jerk at me.
My counterpoint is this: communication involves two things, someone stating a message and someone receiving a message. You are doing part one. Is part two occurring? It may be because of certain conventions in your codebase or team, but I've personally never seen any used in that manner ever, so I would not receive your intended message.
If I were in the same situation I would use unknown and add a comment stating that the type is of no importance since I'm only worried about the keys. That way my message is clear and I prevent future developers from having to debug code where they assume the value is of a certain type and start accessing parameters and methods that do not exist.
const anyToNeverHelper = <T,>(t: any): number & T => t
const absurd: never = anyToNeverHelper<never>(0)Other way around: everything is assignable to the top type (`unknown`), and the bottom type (`never`) is assignable to everything.
> It’s also a good example of how the top type `any` casts to whatever you choose
Which is precisely why `any` isn't the top type: if you allow `any`, then types no longer form a lattice, and there is no bottom or top.
If you restrict yourself to a sound fragment, then `unknown` is the top type. Compare `(x: unknown) => boolean` (two inhabitants up to function extensionality) to `(x: any) => boolean` (infinitely many inhabitants).
> And it works the same way with `unknown`, which it also should because once something is known to be part of the null set it should stay known as the null set.
But `unknown` isn't the null set: it's the "set" (insofar as we're pretending that types are sets of values, which isn't quite true) of all terms.
Yep, sorry that’s what I meant.
> Which is precisely why `any` isn't the top type: if you allow `any`, then types no longer form a lattice, and there is no bottom or top.
I’m not sure I understand.
> If you restrict yourself to a sound fragment, then `unknown` is the top type. Compare `(x: unknown) => boolean` (two inhabitants up to function extensionality) to `(x: any) => boolean` (infinitely many inhabitants).
Sure, but I was referring to an exception I make for ignored parts of a type, in a type param. This example would be more analogous as:
<T extends (x: unknown, ...rest: any[]) => boolean>
Which I hope makes my exception more clear, even if you don’t agree with it. It hopefully communicates that x is of interest and rest is not.> But `unknown` isn't the null set: it's the "set" (insofar as we're pretending that types are sets of values, which isn't quite true) of all terms.
I was referring to never as the null set. Once you narrow anything—any, unknown, etc—to never, you can’t widen it to anything (without an unsafe cast of course).
Valid feedback. I even thought of adding it myself, because implied stuff isn’t obvious. I felt it worth communicating because there’s value in what’s implied that isn’t available in the type system. To the extent I have team members consuming the same code, I would definitely communicate the intent. To the extent I have reviewers who read the code, I do discuss it.
To the extent this is in a type parameter position, the onus is on the person writing the function signature and… well if they don’t want a footgun, they have every opportunity to not gun their foot. But that’s entirely opt in by the time they’ve reached that point.
I will offer this as a middle ground that I'm not even 100% sure will work since I'm not in front of a TypeScript interpreter.
What about just defining it as object? Would work for object.Keys, but not sure how the function consumers would get along with it.
That’s pretty much the intent of the constraint. I don’t have time to sit with a type checker right now, but I don’t think we disagree as much as you might think. I arrived at this from years of trying to find the best way to express types which are as strict as possible with as much clarity as possible.
Unfortunately the object type is basically any non-null value, as is {}. They both intuitively mean what I want. They also inherently allow PropertyKey keys, which is effectively Record<string | number | symbol, any>, which is looser than the “dictionary” type I often want to accept in these scenarios.
A better question (for me, and maybe you and maybe all of us who want type certainties) is why we even accept dictionaries in object shapes when Map is the obvious expression of that type. I’ve repeatedly wanted that and shied away from it because it requires too much change for very little gain.
That way the function isn't asking for parameters it doesn't really care about.
Though I do get the appeal of having the function call object.Keys if it's called frequently so as not to have to sprinkle that call everywhere.
Probably the only other reason I've seen is "JSON interop" is "hard" because Map doesn't natively serialize. I think `new Map(Object.entries(oldDictionaryObject))` and `Object.fromEntries(someMap.entries())` are sufficient for most serializer boundary cases (even without feeling fancy and doing that as a true JSON revivifier/resolver pair).