Skip to content

Instantly share code, notes, and snippets.

@webstrand
Created January 26, 2019 03:40
Show Gist options
  • Save webstrand/5fd2d7e413f05d4fdd0a94d7071a28a5 to your computer and use it in GitHub Desktop.
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
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