Skip to content

Instantly share code, notes, and snippets.

@ryankshaw
Last active February 2, 2023 19:51
Show Gist options
  • Save ryankshaw/0766f1c2832f34bbac30e04ba8d13620 to your computer and use it in GitHub Desktop.
Save ryankshaw/0766f1c2832f34bbac30e04ba8d13620 to your computer and use it in GitHub Desktop.
a couple typescript type-aware helpers for iterating objects
type PickByValue<T, V> = Pick<
T,
{ [K in keyof T]: T[K] extends V ? K : never }[keyof T]
>
type Entries<T> = {
[K in keyof T]: [keyof PickByValue<T, T[K]>, T[K]]
}[keyof T][]
/**
* A better Object.entries _BUT ONLY FOR OBJECTS YOU KNOW DON'T HAVE EXTRA PROPERTIES_,
*
* Explanation of why Typescript doesn't give a stronger type to Object.entries:
*
* When you have a type like type Obj = {a: number, b: string, c: number}, it's only
* guaranteed that a value has those properties; it is not guaranteed that the value does not
* also have other properties. For example, the value {a: 1, b: 'foo', c: 2, d: false}
* is assignable to the type Obj (excess property checking for object literals aside).
*
* In this case Object.entries would return an array containing the element ['d', false].
* The type Entries<Obj> says this cannot happen, but in fact it can happen; so Entries<T>
* is not a sound return type for Object.entries in general. You should only use this
* function when you yourself know that the values will have no excess properties.
*
* from: https://stackoverflow.com/questions/60141960/typescript-key-value-relation-preserving-object-entries-type
*/
export const entries = <T extends object>(obj: T) =>
Object.entries(obj) as Entries<T>
/**
* The return type of Object.keys({foo: 1, bar: 2}) is `string[]`.
* If you use this, the return type will be `("foo" | "bar")[]`.
* from: https://stackoverflow.com/a/59459000
*/
export const keys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
// USAGE:
const foo = {
bar: Math.random() > 0.5 ? 'a' : 'b',
bang: 1,
} as const
Object.entries(foo).forEach(([key, value]) => {
// key is `string` (not the more narrowed `'bar' | 'bang'`)
// value is `'a' | 'b' | 1`
if (key === 'bar') {
// no narrowing of value
} else if (key === 'bang') {
// no narrowing of value
} else {
// typescript doesn't know we can't get here. key is `string`
}
})
entries(foo).forEach(([key, value]) => {
// key is `'bar' | 'bang'`
if (key === 'bar') {
// value is `'a' | 'b'` (the narrowest)
} else if (key === 'bang') {
// value is `1` (the narrowest)
} else {
// typescript knows we can't get here. key is `never`
}
})
keys(foo).forEach((key) => {
if (key === 'bar') {
const value = foo[key]
// value is `'a' | 'b'` (the narrowest)
} else if (key === 'bang') {
const value = foo[key]
// value is `1` (the narrowest)
} else {
// typescript knows we can't get here. key is `never`
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment