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 type object or we don't know the specific keys on the object), then the hasKey function doesn't work as well.
For example, take this function:
functionfn(o:object,k:string){if(hasKey(o,k)){o[k]=3;// Error: Type '3' is not assignable to type 'never'.}}
In the above code, the key k gets narrowed to the type never because keyof object is never (since the object 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 to string | number | symbol.
With this helper, this code will work fine:
functionfn(o:object,k:string){if(hasKey(o,k)){o[k]=3;// This is 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.
constk='a';consto={a:0};if(hasOwnProperty(o,k)){constresult=o[k];console.log(result);// typeof result is number}
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 the hasOwnProperty 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:
exportfunctionhasOwnProperty<Oextendsobject,KextendsPropertyKey>(obj:O,key:K,):objisObjectWithKey<O,K>{returnObject.prototype.hasOwnProperty.call(obj,key);}/**
* This utility type takes an object type O and a key type K. If K is a known
* key on O, it evaluates to the original object O. Otherwise, it evaluates to
* the object O with an additional key K that has an `unknown` value.
*/typeObjectWithKey<Oextendsobject,KextendsPropertyKey>=KextendskeyofO?O:O&Record<K,unknown>;
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).