Skip to content

Instantly share code, notes, and snippets.

@thejohnfreeman
Last active September 4, 2018 05:08
Show Gist options
  • Save thejohnfreeman/3fd0ae6c50cdcec52bbae60576a952af to your computer and use it in GitHub Desktop.
Save thejohnfreeman/3fd0ae6c50cdcec52bbae60576a952af to your computer and use it in GitHub Desktop.
Typescript puzzle
// 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
// 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