Last active
September 4, 2018 05:08
-
-
Save thejohnfreeman/3fd0ae6c50cdcec52bbae60576a952af to your computer and use it in GitHub Desktop.
Typescript puzzle
This file contains hidden or 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
// This first solution mostly works, | |
// but if I think of the Pair in RecordOfPairs as mapping an input A to an output B, | |
// then I want RecordOfPairs to take as its type parameters | |
// a record of input As and a record of output Bs, | |
// the return types of `ays` and `bees`. | |
// Then, RecordOfPairs is mapping its first type parameter to its second, | |
// just like Pair. | |
function pluck<T extends { [k1 in K1]: { [k2 in K2]: T[k1][k2] } }, K1 extends keyof T, K2 extends keyof T[K1]> | |
(key: K2, obj: T): { [k in K1]: T[k][K2] } { | |
// Typescript doesn't work well with lazily initialized objects | |
// Use T[k] not T[K1] to avoid getting a union of all types | |
const result: { [k in K1]: T[k][K2] } = {} as any | |
// Type assertion is required as Object.keys returns string[], not Array<keyof T> | |
Object.keys(obj).forEach(k => result[k] = obj[k as K1][key]) | |
return result | |
} | |
type Pair<A, B> = { a: A, b: B } | |
class RecordOfPairs< | |
G extends { [k: string]: Pair<any, any> } | |
> { | |
constructor( | |
private readonly pairs: G, | |
) { } | |
public getPair<K extends keyof G>( | |
key: K | |
): Pair<G[K]['a'], G[K]['b']> { | |
return this.pairs[key] | |
} | |
// Leave off the type, it will be inferred correctly. | |
// Typescript won't protect you from misstating the return type | |
// (which is { [k in keyof G]: G[k]['a'] }). | |
public get ays(): { [k in keyof G]: G[k]['a'] } { | |
return pluck('a', this.pairs) | |
} | |
public get bees() { | |
return pluck('b', this.pairs) | |
} | |
} | |
type Ctor<A, B> = (a: A) => Pair<A, B> | |
const buildRecordOfPairs = | |
<K extends string> | |
(ctors: { [k in K]: Ctor<any, any> }) => | |
(args: { [k in K]: FirstParameter<typeof ctors[k]> }) | |
: RecordOfPairs<{ [k in K]: ReturnType<typeof ctors[k]>}> => { | |
//) { | |
const pairs: { [k in K]: ReturnType<typeof ctors[k]> } = {} as any | |
Object.keys(ctors).forEach(k => ((pairs[k] as any) = (ctors[k] as any)(args[k] as any))) | |
return new RecordOfPairs(pairs) | |
} | |
type FirstParameter<F> = | |
F extends (a: infer A, ...args: any[]) => any ? A : never | |
const obj = { | |
a: { b: 1 }, | |
c: { b: '2'}, | |
} | |
const plucked: { a: number, c: string } = pluck('b', obj) | |
const record = new RecordOfPairs({ | |
c: { a: '1', b: 1 }, | |
d: { a: 2, b: '2'}, | |
}) | |
const good: Pair<string, number> = record.getPair('c') | |
//const bad: Pair<number, string> = record.getPair('c') | |
const aaa: {c: string, d: number} = record.ays | |
const bbb = record.bees | |
const ctors = { | |
c: (a: string) => ({ a: a, b: 1 }), | |
d: (a: number) => ({ a: a, b: '2' }), | |
} | |
const args = { | |
c: '1', | |
d: 2, | |
} | |
const record2 = buildRecordOfPairs(ctors)(args) | |
const good2: Pair<string, number> = record2.getPair('c') | |
const aaa2: {c: string, d: number} = record2.ays |
This file contains hidden or 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
// How do we declare the type of this function? | |
function pluck(key, obj) { | |
const result = {} | |
Object.keys(obj).forEach(k => result[k] = obj[k][key]) | |
return result | |
// Using Ramda: | |
// return R.map(v => v[key], obj) | |
} | |
type Pair<A, B> = { a: A, b: B } | |
// How should we declare these type parameters? | |
class RecordOfPairs< | |
K extends string, | |
A extends Record<K, any>, | |
B extends Record<K, any>, | |
> { | |
// A class holding a mapping from string keys to pairs. | |
// Each pair has its own type; | |
// they do not necessarily have the same type. | |
constructor( | |
private readonly pairs: { [k in K]: Pair<A[k], B[k]> }, | |
) { } | |
public getPair(key: K): Pair<A[K], B[K]> { | |
return this.pairs[key] | |
} | |
// We have two functions to project | |
// from the mapping of strings to pairs | |
// to a mapping of strings to one property of the pair. | |
public get ays(): A { | |
return pluck('a', this.pairs) | |
} | |
public get bees(): B { | |
return pluck('b', this.pairs) | |
} | |
} | |
// A Ctor takes the first part of a pair | |
// and constructs the whole pair. | |
type Ctor<A, B> = (a: A) => Pair<A, B> | |
// How do we declare the type of this function? | |
// It takes a record of Ctors, | |
// and a record of arguments to those Ctors, | |
// and constructs a RecordOfPairs. | |
function buildRecordOfPairs<K, A, B>( | |
ctors: { [k in K]: Ctor<A[K], B[K]> }, | |
args: Record<K, A[K]>, | |
): RecordOfPairs<K, A, B> { | |
const pairs = {} | |
Object.keys(ctors).forEach(k => pairs[k] = ctors[k](args[k])) | |
// Using Ramda: | |
// const pairs = R.mapObjIndexed( | |
// (ctor, key) => ctor(args[key]), | |
// ctors, | |
// ) | |
return new RecordOfPairs(pairs) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment