Created
August 7, 2019 20:03
-
-
Save thejohnfreeman/31c890be622596bcd30004a9e5cb52c0 to your computer and use it in GitHub Desktop.
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
// Objects.ts | |
type ObjectOf<T> = { | |
[key: string]: T; | |
}; | |
function map<A, B>( | |
input: ObjectOf<A>, | |
f: (a: A, key: string) => B | |
): ObjectOf<B> { | |
const output = {} as ObjectOf<B>; | |
Object.keys(input).forEach(key => { | |
output[key] = f(input[key], key); | |
}); | |
return output; | |
} | |
function zipWith<A, B, C>( | |
ayys: ObjectOf<A>, | |
bees: ObjectOf<B>, | |
f: (a: A, b: B) => C | |
): ObjectOf<C> { | |
const output = {} as ObjectOf<C>; | |
Object.keys(ayys).forEach(key => { | |
output[key] = f(ayys[key], bees[key]); | |
}); | |
return output; | |
} | |
type Pluck<F extends ObjectOf<object>, A extends keyof F[keyof F]> = { | |
[K in keyof F]: F[K][A] | |
}; | |
function pluck<F extends ObjectOf<object>, A extends keyof F[keyof F]>( | |
functor: F, | |
attribute: A | |
): Pluck<F, A> { | |
const output = {} as Pluck<F, A>; | |
Object.keys(functor).forEach((key: keyof F) => { | |
output[key] = functor[key][attribute]; | |
}); | |
return output; | |
} | |
pluck({ a: { k: 1 }, b: { k: "c" } }, "k").a; | |
pluck({ a: { k: 1 }, b: { k: "c" } }, "k").b; | |
pluck({ a: { k: 1 }, b: { k: "c" } }, "k1"); | |
// ViewModel.ts | |
interface ViewModel<V, R> { | |
value: V; | |
repr: R; | |
} | |
interface ViewModelConstructor< | |
I, | |
V extends I = I, | |
R = V, | |
M extends ViewModel<V, R> = ViewModel<V, R> | |
> { | |
construct(initValue?: M | I): M; | |
} | |
// FieldViewModel.ts | |
class FieldViewModel<V, R = V> implements ViewModel<V, R> { | |
public constructor(public value: V, public repr: R) {} | |
} | |
type FieldViewModelConstructor< | |
I, | |
V extends I = I, | |
R = V | |
> = ViewModelConstructor<I, V, R, FieldViewModel<V, R>>; | |
// ArrayViewModel.ts | |
class ArrayViewModel<V, R, M extends ViewModel<V, R>> | |
implements ViewModel<V[], R[]> { | |
public constructor(public readonly items: M[]) {} | |
public value: V[] = this.items.map(item => item.value); | |
public repr: R[] = this.items.map(item => item.repr); | |
} | |
type ArrayViewModelConstructor< | |
I, | |
V extends I = I, | |
R = V, | |
M extends ViewModel<V, R> = ViewModel<V, R> | |
> = ViewModelConstructor<I[], V[], R[], ArrayViewModel<V, R, M>>; | |
// There is no ViewModelConstructorArray because arrays are homogeneous. | |
// They are not constructed with an array of ViewModelConstructors. | |
// They are constructed with a single element ViewModelConstructor. | |
// GroupViewModel.ts | |
type ViewModelGroup = ObjectOf<ViewModel<unknown, unknown>>; | |
type ValueGroup<G extends ViewModelGroup> = Pluck<G, "value">; | |
type ReprGroup<G extends ViewModelGroup> = Pluck<G, "repr">; | |
class GroupViewModel<G extends ViewModelGroup> | |
implements ViewModel<ValueGroup<G>, ReprGroup<G>> { | |
public constructor(public readonly members: G) {} | |
public value: ValueGroup<G> = pluck(this.members, "value"); | |
public repr: ReprGroup<G> = pluck(this.members, "repr"); | |
} | |
// TODO: Use a real Proxy to make this easier. | |
function makeGroupProxy<G extends ViewModelGroup>( | |
gvm: GroupViewModel<G> | |
): ValueGroup<G> { | |
const proxy = {} as ValueGroup<G>; | |
Object.keys(gvm.members).forEach(key => { | |
Object.defineProperty(proxy, key, { | |
enumerable: true, | |
get: () => gvm.members[key].value, | |
set: value => (gvm.members[key].value = value) | |
}); | |
}); | |
return Object.freeze(proxy); | |
} | |
type GroupViewModelConstructor<G extends ViewModelGroup> = ViewModelConstructor< | |
Partial<ValueGroup<G>>, | |
ValueGroup<G>, | |
ReprGroup<G>, | |
GroupViewModel<G> | |
>; | |
type ViewModelConstructorGroup = ObjectOf< | |
ViewModelConstructor<unknown, unknown, unknown, ViewModel<unknown, unknown>> | |
>; | |
type ViewModelGroupIsomorphicTo<CG extends ViewModelConstructorGroup> = { | |
[K in keyof CG]: ReturnType<CG[K]["construct"]> | |
}; | |
// ViewModels.ts | |
type IntegerViewModel = FieldViewModel<number, string>; | |
function integer(): FieldViewModelConstructor<number, number, string> { | |
return { | |
construct(initValue: IntegerViewModel | number): IntegerViewModel { | |
return initValue instanceof FieldViewModel | |
? initValue | |
: new FieldViewModel(initValue, "" + initValue); | |
} | |
}; | |
} | |
export function array< | |
I, | |
V extends I = I, | |
R = V, | |
M extends ViewModel<V, R> = ViewModel<V, R> | |
>( | |
ctor: ViewModelConstructor<I, V, R, M> | |
): ArrayViewModelConstructor<I, V, R, M> { | |
return { | |
construct( | |
initValues: ArrayViewModel<V, R, M> | I[] = [] | |
): ArrayViewModel<V, R, M> { | |
return initValues instanceof ArrayViewModel | |
? initValues | |
: new ArrayViewModel( | |
initValues.map(initValue => ctor.construct(initValue)) | |
); | |
} | |
}; | |
} | |
function group<G extends ViewModelConstructorGroup>( | |
ctors: G | |
): GroupViewModelConstructor<ViewModelGroupIsomorphicTo<G>> { | |
return { | |
construct( | |
initValues: | |
| GroupViewModel<ViewModelGroupIsomorphicTo<G>> | |
| Partial<ValueGroup<ViewModelGroupIsomorphicTo<G>>> = {} | |
): GroupViewModel<ViewModelGroupIsomorphicTo<G>> { | |
return initValues instanceof GroupViewModel | |
? initValues | |
: new GroupViewModel(map(ctors, (ctor, key) => | |
ctor.construct(initValues[key]) | |
) as ViewModelGroupIsomorphicTo<G>); | |
} | |
}; | |
} | |
// tests.ts | |
const ic1 = integer(); | |
const i1 = ic1.construct(0); | |
i1.repr; | |
i1.value; | |
const avmc1 = array(integer()); | |
const avm1 = avmc1.construct([1, 2, 3]); | |
console.log('avm1.repr = ["1", "2", "3"]', avm1.repr); | |
console.log("avm1.value = [1, 2, 3]", avm1.value); | |
const r0 = avm1.repr[0]; | |
const v1 = avm1.value[0]; | |
const avm2 = avmc1.construct(avm1); | |
console.log('avm2.repr = ["1", "2", "3"]', avm2.repr); | |
console.log("avm2.value = [1, 2, 3]", avm2.value); | |
console.log("avm2.items", avm2.items); | |
const gvmc1 = group({ | |
age: integer() | |
}); | |
const gvm1 = gvmc1.construct({ age: 32 }); | |
console.log("gvm1.members.age :: FieldViewModel<number, string>", gvm1.members | |
.age as FieldViewModel<number, string>); | |
console.log( | |
"gvm1.value.age = 32 :: number", | |
gvm1.value.age as number, | |
typeof gvm1.value.age | |
); | |
console.log( | |
'gvm1.repr.age = "32" :: string', | |
gvm1.repr.age as string, | |
typeof gvm1.repr.age | |
); | |
const avmc2 = array(gvmc1); | |
const avm3 = avmc2.construct([{ age: 32 }]); | |
console.log( | |
"avm3.items :: GroupViewModel<{ age: FieldViewModel<number, string> }>[]", | |
avm3.items | |
); | |
console.log( | |
"avm3.value[0].age = 32 :: number", | |
avm3.value[0].age as number, | |
typeof avm3.value[0].age | |
); | |
console.log( | |
'avm3.repr[0].age = "32" :: string', | |
avm3.repr[0].age as string, | |
typeof avm3.repr[0].age | |
); | |
const gvmc2 = group({ | |
series: array(integer()) | |
}); | |
const gvm2 = gvmc2.construct({ series: [1, 2, 3] }); | |
console.log("gvm2.value.series = [1, 2, 3]", gvm2.value.series); | |
console.log( | |
"gvm2.value.series[0] = 1 :: number", | |
gvm2.value.series[0] as number, | |
typeof gvm2.value.series[0] | |
); | |
gvmc2.construct(); | |
gvmc2.construct({}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment