12 ms·
Why doesn't TypeScript properly type Object.keys?
- cronin101 3y agoNeat, I hadn’t thought about the covariance/contravariance implications of structural typing when getting irritated by this shortcoming yesterday! Small nitpick, the `KeyboardShortcutEvent` snippet at the end would be a great place to showcase `Pick<Foo, “bar”|”baz”>`
- awayto 3y agoI thought of using Partial<T> for the last example, as well. Obviously this has its own implications; right tool for the job and all that.
- mirekrusin 3y agoFlow has exact object types as dedicated annotation/kind. This problem is much bigger. You can for example easily imagine api where you provide where clause that is converted to sql form. You may declare type to expect only certain parameters, but it won't help you at the type level as anything extra can be passed. This can be abused to extract some sensitive information. You're forced to emulate exactness by always destructuring objects - not great.
- __ryan__ 3y agoYou're forced to emulate exactness by always destructuring objects - not great. It’s almost like using objects as enumerable maps is an anti-pattern.
- toxik 3y agoThe problem has nothing to do with objects. The problem is, how do you type check something like sprintf without ad hoc type rules?
- __ryan__ 3y agoI don’t think I’m following well enough to provide a meaningful response. This is not meant as an argument against what you’re saying, because I know you were just giving an example, but I found this and thought you may find it interesting: https://www.hacklewayne.com/a-truly-strongly-typed-printf-in-typescript https://www.hacklewayne.com/a-truly-strongly-typed-printf-in...
- jitl 3y agoTypescript can check sprintf though using template string types: https://www.hacklewayne.com/a-truly-strongly-typed-printf-in-typescript https://www.hacklewayne.com/a-truly-strongly-typed-printf-in...
- nine_k 3y agoMeanwhile, both Rust [1] and Haskell [2] manage to implement statically type-safe string interpolation. [1]: https://willcrichton.net/notes/type-safe-printf/ https://willcrichton.net/notes/type-safe-printf/ [2]: https://hackage.haskell.org/package/formatting https://hackage.haskell.org/package/formatting
- Spivak 3y agoYes but the Rust example is an ad hoc type rule implemented behind the macro. You can make it type-safe but you lose the ability to have a formatting language in the string itself.
- mirekrusin 3y agoRust has explicit support for it in the compiler, which is not great. Zig does it the right way - it's defined in zig itself, no special cases in compiler like in Rust.
- mirekrusin 3y agoTo extend on it a bit, flow has really good story with: 1. nominal types for classes (with correct variance/liskov substitution principles) 2. structural types for the rest 3. explicit annotation for exact structural types This setup is brilliant.
- valenterry 3y agoWell, there should just be a function `Type.keys` that accepts a type and, at the typelevel/compiletime, finds the keys and returns only those that the type we are using has, not the underlying object at runtime. Of course, that only works if we know the type statically just like in the example of the post. But that is exactly the usecase that is needed in this example and then the type-errors are gone.
- __ryan__ 3y agoRuntime type information is against the goals of TypeScript. https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi... Edit: Non-goals: … 5. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
- danielheath 3y agoDid the comment you're replying to get edited? They are pretty explicitly talking about statically known type information, not runtime.
- __ryan__ 3y agoThey’re suggesting outputting runtime type information by emitting different code based on the type of the value passed to their hypothetical “Type.keys()” function.
- danielheath 3y agoAhh, that makes sense then; thanks for clarifying.
- tentacleuno 3y agoWell that's interesting. That means decorator metadata was a non-goal, despite being supported for ages.
- quickthrower2 3y agoOf course the validateOptions function is nonsensical. It just checks if any null values exist. You could iterate Object.values() to do the same. This needs a better motivating example.
- styfle 3y agoNice explanation! Although I’m surprised there is no mention of the best solution for iterating both keys and values: for (const [key, val] of Object.entries(obj)) { console.log(key, val) } https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- maxiwer 3y agoGreat, neat way!
- skrebbel 3y agoNote that Object.entries also returns keys as strings (and not keyof T), just like Object.keys does. It has the exact same problem and rationale as explained in this article. This means that if you just want to loop, you’re good, but the moment you need something fancier than a console.log you might need the keys to be typed stronger again.
- andrewaylett 3y agoIn many cases though, you only need a correctly-typed key in order to index into the object to get the value -- if you've already got the value, you're fine. And if you need to limit yourself to keys that are defined on the type you're using (rather than arbitrary keys which might exist on the object at runtime) then you need to check anyway.
- modestygrime 3y agoObject.entries has the same problem: https://stackoverflow.com/q/60141960 https://stackoverflow.com/q/60141960
- Dylan16807 3y agoIt has some of the same problems. It doesn't have the problem of being unable to read obj[key] because typescript doesn't realize it's the same object.
- _ZeD_ 3y agoSo the real reason is "because of the limitation of the structural typing instead of the nominal one"?
- layer8 3y agoNo, the reason is that the properties enumeration works on the dynamic type and not on the static type.
- recursive 3y agoYou can do this with all nominal types too. Imagine types Parent and Child. Child has more properties, yet satisfies the contract of Parent. If you enumerate all the properties of some arbitrary Parent p, you may get some extra properties.
- _ZeD_ 3y agothat's not how nominal types work: the properties looped over will be only the one of Parent
- bazoom42 3y agoIn Java or C# you would need to use reflection to loop over the members of an object, but AFAIK this would yield the members of the current instance, not the nominal type.
- recursive 3y agoNo, sorry, you're not right. Here's some C# you can run in your browser. https://dotnetfiddle.net/ZeSNkA https://dotnetfiddle.net/ZeSNkA
- afiori 3y agoThe article says that it is because of structural typing, but actually it is because in TS object subtype automatically. It has little to do with structural typing per se; you would have the same behaviour with nominal classes* and inheritance. * this is likely why OOP languages have _final_ classes
- asynchronous 3y agoIf typescript had better errors then I probably would support it more as a development language. As it stands it exists almost exclusively as overhead on most user interfaces.
- jimmychoozyx 3y agoI agree-- in terms of it being almost exclusively overhead. Personally, I hate typescript. So much redundancy. It's not insanely awful, but just awful and unnecessary enough (except large corporate projects with significant developer churn) that it's a pet peeve of mine. "Hey, let's just write the same thing 4 times so you know it's a string! Because quotation marks aren't clear enough! Yay!!" "I must redundantly ensure you know this number is a number! Even though you can easily convert it to one if you need to, and move on! Yay" I'm bummed that it is becoming so common and popular in the industry.
- beebeepka 3y agoI don't mind TS but sometimes it kinda feels like a cult. Certain personalities love to overcomplicate things for zero actual value other than they get to feel like shamans or something
- PUSH_AX 3y agoAgreed, and it’s extremely difficult to have any objective TS discourse in TS communities. They are extremely dismissive of criticism, constructive or not.
- crgwbr 3y agoTS is definitely not zero actual value. The decrease in bugs my team has seen since migrating our JS apps to strict-mode TS is remarkable. It’s been ~5 years since then and I can’t imagine going back.
- beebeepka 3y agoI did not say TS is if no value. I said overdoing it is of no value. Perhaps I should have phrased that better.
- msoad 3y agoSimilar reason why you can’t have exception types in TypeScript. The underlying runtime and language doesn’t allow it to work.
- msoad 3y agoHere is more explaining of what I mean. Coming from other languages you might think TypeScript can add a `throws` type annotation so caller can expect the type of error in the `catch` block. Something like this: function parse(input: string): string, throws SyntaxError {} However in JavaScript we can not guarantee that when calling `parse` the only error you might get is the error that this function throws. So there is no point in adding type annotation
- cjonas 3y agoIn the keyboard event example of only specifying the properties you want to test against, it would be better to use `Pick` to create a relational type off the original. That way if the property or types change (unlikely in this case), your type system will remain up-to-date.
- baerrie 3y agoYep, this highlights imo the real value of TS, not type safety for data integrity but instead for ease of development and refactoring
- webstrand 3y agoI work around the issue of `Object.keys` being unsafe by having a function which takes a list of keys and verifies at compile time that it contains all of the known keys on the object. Excess keys won't be iterated over, at runtime, but I consider this to be a boon. https://tsplay.dev/W47y1m https://tsplay.dev/W47y1m
- a_wild_dandan 3y agoI use type guards and the `io-ts` package to get both compile-time and runtime type checking: import * as t from 'io-ts'; /* -------- Runtime type definitions -------- */ export const FooSchema = t.type({ someStringKey: t.string, someNumericKey: t.number, someLiteralKey: t.union([t.literal(1), t.literal(8675309)]) someOtherType: t.type({ anotherNumber: t.number, yetAnotherType: BarSchema }) }) export const BarSchema = t.type({ ... }) /* ---- Run-of-the-mill Typescript types ---- */ export type Foo = t.Typeof<typeof FooSchema>; export type Bar = t.Typeof<typeof BarSchema>; // Static usage export const Baz: Foo = { someStringKey: 5 } // Compile error! Missing keys/etc // Runtime usage export const isFooType = (input: any): input is Foo => FooSchema.validate(input) if (!isFooType(someUserInput)) return server.send(404); You can enforce exact type shape, or just ignore excess keys, etc. It's pretty handy, and saves me so much work maintaining two type systems, a static one for development and a runtime one for production. All of the typical caveats apply: consider runtime performance cost, recognize that deep/nuanced types will require a deeper/nuanced understanding of io-ts, there are limitations, alternative libraries with different tradeoffs, and so on. I like it. :)
- deleted 3y ago[deleted]
- nine_k 3y agoSo, the idea was to make every function's arguments covariant, instead of having to write it explicitly. `KeyboardEvent` is assignable to `KeyboardShortcutEvent` directly, without saying that you actually are ready to receive `<T extends KeyboardShortcutEvent>` or something even more elaborate. It's a design decision which is a tradeoff, like most design decisions. I won't mind to have instead a more succinct syntax for covariance, like `event: KeyboardShortcutEvent+`, maybe along with a similar syntax for contravariance (fewer properties). OTOH declaring a well-typed helper function and instead of `Object.keys(x)` write some `TypedObject.keys(x)` does not seem hard to me.
- baerrie 3y agoThe author doesn’t address a common work-around for Object.keys() which is a type guard. https://www.typescriptlang.org/docs/handbook/advanced-types.html https://www.typescriptlang.org/docs/handbook/advanced-types....
- mock-possum 3y agoType guarding is definitely a pain point moving from JS to TS
- baerrie 3y agoFor sure. I think most devs are afraid to add more conditionality but even JS warrants a ton of undefined checks, etc because of the language’s generic nature
- joshkel 3y agoA small addition: Structural typing means that TypeScript normally allows extra properties, but in contexts where you're immediately declaring and using a type, TypeScript will complain. For example: `const user = { name: 'Bob', city: 'Reykjavík' }; saveUser(user)` is allowed because of structural typing. But `const user: User = { name: 'Bob', city: 'Reykjavík' }` throws a compile error; you're telling that you're specifying a `User`, but the value you give is not (just) a `User`.
- draw_down 3y ago[dead]
- jitl 3y agoAt Notion, we re-type Object.keys and Object.entries for the situations where you want more specific key types. For example, iterating over a `Record<ThingyIdType, ThingyUpdate>` while preserving the well-typed ID key, or iterating over a `SchemaDefinition<Thingy>` which is known to be statically declared as an exact object. /** * Like Object.keys, but unsound in exchange for more convenience. * * Casts the result of Object.keys to the known keys of an object type, * even though JavaScript objects may contain additional keys. * * Only use this function when you know/control the provenance of the object * you're iterating, and can verify it contains exactly the keys declared * to the type system. * * Example: * ``` * const o = {x: "ok", y: 10} * o["z"] = "UNTRACKED_KEY" * const safeKeys = Object.keys(o) * const unsafeKeys = objectKeys(o) * ``` * => const safeKeys: string[] * => const unsafeKeys: ("x" | "y")[] // Missing "z" */ export const objectKeys = Object.keys as <T>(obj: T) => Array<keyof T> /** * The type of a single item in `Object.entries<T>(value: T)`. * * Example: * ``` * interface T {x: string; y: number} * type T2 = ObjectEntry<T> * ``` * => type T2 = ["x", string] | ["y", number] */ export type ObjectEntry<T> = { // Without Exclude<keyof T, undefined>, this type produces `ExpectedEntries | undefined` // if T has any optional keys. [K in Exclude<keyof T, undefined>]: [K, T[K]] }[Exclude<keyof T, undefined>] /** * Like Object.entries, but returns a more specific type which can be less safe. * * Example: * ``` * const o = {x: "ok", y: 10} * const unsafeEntries = Object.entries(o) * const safeEntries = objectEntries(o) * ``` * => const unsafeEntries: [string, string | number][] * => const safeEntries: ObjectEntry<{ * x: string; * y: number; * }>[] * * See `ObjectEntry` above. * * Note that Object.entries collapses all possible values into a single union * while objectEntries results in a union of 2-tuples. */ export const objectEntries = Object.entries as <T>( o: T ) => Array<ObjectEntry<T>>
- presentation 3y agoAt my company we just have a wrapper around the Object.keys function (and other similar functions) that provide a more specific type in case you are sure it won’t be a problem (which we generally are for objects we’ve created ourselves).
- jdthedisciple 3y agoUnpopular opinion: TypeScript is overrated. After 1 year of Vanilla JS you will know all the types by heart. All the necessary static type analysis will be happening automatically in your brain. No cluttered type syntax and linter wars needed.
- fbrchps 3y agoBiggest advantage I've seen from using it, is the ability to quickly get into old code. Sure, well I'm writing it. I have a mental understanding of all the different types, but I don't two years down the line. Also, when multiple people are working on the same code, it's nice that they all are forced to spell things the same way...
- jdthedisciple 3y agoI recently created a new NextJS project. For my little site I needed an input element with type=file. So I was getting started, with TypeScript and all, like the swaggest of web developers. Came the moment for the actual file input: TypeScript REFUSED to accept the input element's type as "HTMLInputElement" when that is literally its type. After TypeScript eating up about 1 hour or so of my time, I decided to get rid of that piece of sh*t for squiggly underlining all my code in Red simply because it's too retarded to understand it. Any of the TypeScript lovers care to explain? Needless to say, I ultimately went about doing what I wanted in 5 minutes in VanillaJS and was happy ever after. Call me again when TypeScript does its job correctly.
- explaininjs 3y agoJust type (… as HTMLInputElement) and be done with it. It’s really not that hard.
- jdthedisciple 3y agoDuh, that's what I did. And every other variant one can possibly think of. To no avail. That's what cost me the hour or two. This works like a charm in a strictly typed language like C#, but TypeScript just hasn't got it down correctly yet.
- CodeWriter23 3y agoMeh. I just type const realkey = key as any as <type> And call it a day. I'm 100% certain I've typed fewer characters of that pattern than that blog post.
- deleted 3y ago[deleted]
- joduplessis 3y agoReason this isn't throwing a warning for me (and working as expected)? On v4.8.4.
- gavinray 3y agoCalling "Object.freeze()" and then asking for the keys/values/entries of a frozen object should be able to return a properly-typed value. But it doesn't do this =(
- williamdclt 3y agoIt couldn’t, no: the object being frozen might contain more attributes than its type describes. Typescript can’t know these extra attributes.
- gavinray 3y agoAt the very least, it could return an intersection type that is equivalent to the known keys in the object so you get autocomplete, but allow other arbitrary properties of unknown type: type FrozenObject<T> = { readonly [K in keyof T]: T[K] } & { readonly [key: string]: unknown }; const x = { foo: 1, bar: "hello" } x["baz"] = "blah" const frozen = Object.freeze(x) as FrozenObject<typeof x> const baz = frozen.baz // No autocomplete for 'baz', but no error either
- spankalee 3y agoThere's an old issue open in TypeScript for _exact_ types, which might help in some cases here: https://github.com/microsoft/TypeScript/issues/12936 https://github.com/microsoft/TypeScript/issues/12936 But there are some problems with that approach, mainly that exact types would be somewhat infectious: A regular type is no longer assignable to its exact type, ie `User` is not assignable to `exact User`, so if you want to use an exact type in a function, you must accept the exact type as a parameter, which spreads to the whole call stack where that parameter is passed. Then union types don't quite work as expected anymore either. I'm pretty happy with the status quo: Object.keys() is unsafe, so either cast your way around it and let the cast be the signal that you're doing something unsafe, or handle unexpected keys explicitly.
- williamdclt 3y agoHaving a generic, easy way to strip off the extra attributes (immutably), that wouldn’t be a problem. Making up an api: Object.exact<T>(obj: T): exact T, eg Object.exact<User>(user) It’s not possible at the moment as all type information is erased at runtime of course.
- deleted 3y ago[deleted]
- ashishb 3y agoPeople using a real language like Rust write a 10-100X performant new tool like `rg/fd` that replaces an existing tool like `grep/find`. People using TypeScript write blog on why Typescript can't even type `Object.Keys`. Someone who only spents time in C++, Go, Java, or Rust would be surprised at this. Types in TypeScript are like Cheese in Cheeseburger, there is a slice of it but the meat is entirely made up of something else.
- kevingadd 3y agoIf you read the blog post you'd see it's not a "TypeScript can't even type Object.Keys" situation but instead a "it is incorrect to assign it the type you want because of how JavaScript works"
- ashishb 3y agoI did read the blog post. The underlying issue might be in JavaScript. But that's an implementation detail. Here's my question to you why is this problem non-existent in c++/Java/go/rust?
- bazoom42 3y agoThose languages all have the same issue since it is a logical consequence of having subtypes. You probaly just don’t realize it becase doing anything similar to the example would require convoluted reflection.
- prmph 3y agoFor those that are well versed in type theory, what is the neatest solution to this in language design, if there is one at all? I'm always bothered by the idea that product types can be "more" than, but never "less" than, the type they are assignable to (contra-variance). For sum types it seems to be the opposite (co-variance), so structural typing is easier. Which reminds me of an idea I've had for a while regarding object oriented inheritance. Why is inheritance always approached in a contra-variant manner? I have frequently wished that inheritance could be conceptualized as a narrowing, rather than expansion, of type. For example, consider an "Animal" object class, which has a "numLegs" attribute. Then "Dog" could inherit from "Animal" by having exactly 4 legs, as a special case. I'm not sure whether this concept has been implemented in any language, but I've encountered situations where it would have been the perfect way to model entities, frequently enough, that I believe it could be very useful.
- comex 3y agoI'm not an expert in type theory, but something like Scala's path-dependent types might work. In the following example (which I copy-pasted from [1]), every object of type Foo has some associated type Bar. You can have multiple objects that are all of type Foo, and they might have different Bar types. Therefore, to refer to a Bar type, you have to reference the specific variable you're talking about – such as in "f1.Bar" below, where f1 is a variable name, not a type name. trait Foo{ class Bar def doNothing(b: Bar){} } val f1 = new Foo{} val b1 = new f1.Bar() val f2 = new Foo{} val b2 = new f2.Bar() f1.doNothing(b1) //fine f1.doNothing(b2) //won't compile Applying the same idea to TypeScript, you could imagine using 'keyof' on values instead of types. So instead of keys<T extends object>(o: T): (keyof T)[]; it would be something like keys(o: object): (keyof o)[]; And the keys returned for an object would only let you index into that specific object, not any other object even if it had the same type. [1] https://wheaties.github.io/Presentations/Scala-Dep-Types/dependent-types.html https://wheaties.github.io/Presentations/Scala-Dep-Types/dep...
- consilient 3y ago> For those that are well versed in type theory, what is the neatest solution to this in language design, if there is one at all? Row-polymorphism instead of structural subtyping. The idea is that instead of treating `Record<,>` as parameterized by two ordinary types, you parameterize it with a single type-level collection of key-value pairs, and then write functions which are generic over parts of that collection. So the equivalent of a typescript function that works fine over open records would look something like this `<Rest>(obj: { SomeRow, Rest }) => { SomeRow, Rest }`, whereas `(obj: { SomeRow }) => { SomeRow }` says you can't have any extra entries and `<Rest>(obj: { SomeRow, Rest }) => { SomeRow }` says you can have extra entries but they're getting thrown out. This also lets you reuse machinery between records and sums more easily, since sums are also realizations of rows. Purescript and OCaml have good row type support. You can also sort of hack it together in typescript if you're very very careful, but I wouldn't recommend it.
- zengid 3y agoI know this is pedantic but there a logical error in this validation code function validateUser(user: User) { let error = ""; for (const key of Object.keys(user)) { const validate = validators[key]; error ||= validate(user[key]); // <-- only first error is assigned } return error; } // instead I would put something like this.. error += validate(user[key])