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’ve never worked in a Typescript shop, is there any truth to the satire here? The sea of confusing types to solve any problem?
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%.
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.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.
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.
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).
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.
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.
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).
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)
}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.