This is a topic that comes up every now and again, so I figured it'd be useful to write a post about it.
Imagine you have code like this:
// an...
For further actions, you may consider blocking this person and/or reporting abuse
Just sharing my personal trick, which is a bit off topic and hacky.
I usually prefer to handle such edge cases within the objet literal itself, by providing a
default
key.Then I basically lie to the TypeScript compiler by omitting the extra
default
key in my type definitions.I recently added some helper utilities to our codebase based on your advice for the
hasKey
function above. This function works great when you are dealing with object literals (or at least well-defined object types). When the object in question is not well defined (for example, the object is interpreted as the base typeobject
or we don't know the specific keys on the object), then thehasKey
function doesn't work as well.For example, take this function:
In the above code, the key
k
gets narrowed to the typenever
becausekeyof object
isnever
(since theobject
type doesn't have any defined keys).There is, however, a different way to handle this case that uses a type predicate to update the type for the object itself. I found it in this other blog post (fettblog.eu/typescript-hasownproperty), and I have copied a slightly modified version of the code below for reference:
Side note that
PropertyKey
is an included TypeScript es5 lib global type definition that is equavalent tostring | number | symbol
.With this helper, this code will work fine:
Additionally, because of how TypeScript handles intersections with the
unknown
type, this also works well for cases where the object is more well defined.While the approach of
hasKey
for narrowing the key type itself probably still has its uses, I currently think that when it comes to indexing objects, the approach used for thehasOwnProperty
utility is probably more broadly applicable and preferable.If you want to be extra fancy and not add the intersection with
Record<K, unknown>
if the key is known to exist on the object type, you can also use conditional types. For example:Actually, the conditional type doesn't work when either of the arguments is itself a generic type (playground example).
I get a kind of interesting error when using
hasKey
,Any idea what's causing this?
Turns out TS 3.0 broke this 🙃
I'm not quite sure how to fix it at the moment, but if someone else has a solution, I'm all ears. I know Lodash's types are pretty comprehensive, so I might start looking there myself
EDIT: I love it when I figure it out right after posting the comment. It needs to be
string | number | symbol
, since 3.0 expanded the kinds of types that can be used for object keys. We can shorten that tokeyof any
.Updated the blog post. Thanks for the heads up 🙌
Nice! I ended up opting for the union type, just because
any
gives me anxiety . .👍 Might even be more readable that way.
I recently discovered this solution.
This means later on in your code you can do something like this.
Provided we know what type
object
is it'll work.Looking through the code we can see if
object
is typeIOptions
thenObject.typedKeys(object)
will beArray<keyof IOptions>
meaning an array of keys forIOptions
. Because of this ourkey
gets the typekeyof IOptions
or key forIOptions
.That's a neat way to go about it! I'm not a fan of monkey-patching builtins, though
I appreciate the solution as I have a similar issue driving me nuts.
However, this is one aspect of TS I'm not really on board with yet and may never be: you had to write an entirely separate function just to handle the key indexing of a simple object.
This seems like a massive waste of time with little benefit in most cases.
I feel like there is a separation with what our goals are with TypeScript (as developers) and what TS is actually doing in this instance. TS is checking the type of the index value first, rather than resolving that index value and determining if it's valid for our use case.
Basically, I'm forced to check each of the index values whether I want or need to, when really I just need to make sure that value is valid for the argument I'm passing in - it seems the argument's expected type should be the source of truth, not the object's index value's type.
I understand that value still needs to be known to check it against the argument's type, but it would seem identifying that type should be able to be inferred in most cases.
Thanks for the clear explanation here, though!
I quite agree with you.
For this edge case the lack of type inference is a bit disappointing.
Try this bro hope it helps