Created
January 26, 2019 03:40
-
-
Save webstrand/5fd2d7e413f05d4fdd0a94d7071a28a5 to your computer and use it in GitHub Desktop.
Guide on how to extract values from a map using keys from an array, but type-safely
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Thing<T> = { | |
thing: T; | |
} | |
const propertyTableFields = { | |
a: { thing: "A" }, | |
b: { thing: 5 }, | |
c: { thing: "C" }, | |
}; | |
// Least interesting version of selectFields: | |
// | |
// T extends { [key: string]: Thing<Any> } -- Since all types are assignable | |
// to any, we don't need an additional type variable to allow for arbitrary | |
// Thing<_> types. | |
// | |
// K extends (keyof T)[] -- K is an array of some subset of keys of `obj`. | |
// | |
// The return value of selectFields1 is an array of property values in `obj` | |
// without respect to the length or order of keys. However, only properties | |
// that have a corresponding value in `keys` are considered. For example if | |
// `keys` is [ 'a', 'b' ] then the return value is | |
// (typeof obj['a'] | typeof obj['b'])[] | |
declare function selectFields1<T extends { [key: string]: Thing<any> }, K extends (keyof T)[]>(obj: T, keys: K): T[K[number]][]; | |
// A little bit more complicated version of selectFields: | |
// | |
// The return value of selectFields2 does respect the length and order of | |
// `keys`, provided you can manage to pass it a tuple type. The type is | |
// constructed by the numeric keys of `keys` { [N in keyof K]: N } becomes | |
// [ "0", "1", ...] for each item in the tuple `keys`. (If `keys` isn't a | |
// tuple, it becomes an unenlightened number[]). | |
// Each index N is then used to remap each keys[N] to the typeof obj[keys[N]] | |
// via { [N in keyof K]: T[K[N]] }. Unfortunately, the compiler has lost the | |
// knowledge that each value of `keys` is guaranteed to be a keyof obj by | |
// adding a conditional: (K[N] extends keyof T ? K[N] : never). | |
// Thus, when `keys` is [ 'a', 'b' ], the return value is | |
// (typeof obj['a'] | typeof obj['b'])[] | |
// | |
// ...because the compiler has forgotten the real type of `keys` is | |
// [ 'a', 'b'] and has instead represented its type as ('a'|'b')[]. | |
declare function selectFields2<T extends { [key: string]: Thing<any> }, K extends (keyof T)[]>(obj: T, keys: K): { [N in keyof K]: T[K[N] extends keyof T ? K[N] : never] }; | |
// Final version of a type-safe selectFields: | |
// | |
// So we need to convince the compiler that the tuple representation (when | |
// available) really is the best representation of the type of `keys`. | |
// | |
// The compiler can be tricked into thinking that K is really an object full | |
// of numeric keys by doing: K extends (keyof T)[] & {"0": keyof T} | |
// Now `keys` is always a psudeo-tuple and we correctly remap its values, | |
// preserving the length and order of the keys. | |
// | |
// There's still a defect, we can't pass selectFields3 a non-tuple array or | |
// an empty tuple/array. This can be fixed by allowing `keys` to be an array: | |
// K extends (keyof T)[] & { "0": keyof T } | (keyof T)[] | |
// | |
// And that's it! The return value of selectFields3 now respects the length | |
// and order of `keys`. You could do type-safe destructuring assignment: | |
// let [a, b] = selectFields3(propertyTableFields, ['a', 'b']); | |
function selectFields3<T extends { [key: string]: Thing<any> }, K extends (keyof T)[] & { "0": keyof T } | (keyof T)[]>(obj: T, keys: K): { [N in keyof K]: T[K[N] extends keyof T ? K[N] : never] } { | |
// The typings of Array.prototype.map just aren't advanced enough | |
return <any> keys.map(k => obj[k]); | |
} | |
let t0: [] | |
= selectFields3(propertyTableFields, []); | |
let t1: [Thing<string>] | |
= selectFields3(propertyTableFields, ['a']); | |
let t2: [Thing<number>] | |
= selectFields3(propertyTableFields, ['b']); | |
let t3: [Thing<number>, Thing<string>] | |
= selectFields3(propertyTableFields, ['b', 'a']); | |
let t4: [Thing<string>, Thing<number>] | |
= selectFields3(propertyTableFields, ['a', 'b']); | |
const q5: ('a' | 'c')[] = ['a', 'c']; // tsc thinks q5 ought to be string[] | |
let t5: Thing<string>[] | |
= selectFields3(propertyTableFields, q5); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment