I have a domain class, with all the fields being primitives, string, number or boolean. For simplicity, let's work with a bike:
interface Bike {
wheels: number;
isMTB: boolean;
brand: string;
}
In my application, I'm using knockout-mapping
so I can easily make all the fields editable, e.g. wheels
becomes ko.Observable<number>
. I have created therefore a type to help me type it, e.g.
export type MappedObject<T> = {
[key in keyof T]: ko.Observable<T[key]>;
};
I'm usually maintaining a collection of editable bikes, e.g. bikes = ko.observableArray<MappedObject<Bike>>();
And pushing one editable bike like this:
const bike = { wheels: 2, isMTB: true, brand: "Cross" }; // Bike
const editBike = ko.mapping.fromJS(bike); // MappedObject<Bike>
bikes.push(editBike);
When I'm saving my changes, I'm converting the collection of editable bikes into an array of plain bike objects using either ko.toJS
or ko.mapping.toJS
. Both unwraps any observable
fields into their primitive value, so for bikes
defined above, ko.toJS
returns, at runtime, a Bike[]
. However, in Typescript, it doesn't return that, it returns MappedObject<Bike>[]
.
For ko.toJS
there is a definition:
type Unwrapped<T> = T extends ko.Subscribable<infer R> ? R :
T extends Record<any, any> ? { [P in keyof T]: Unwrapped<T[P]> } : T;
export function toJS<T>(rootObject: T): Unwrapped<T>;
For knockout-mapping
, there isn't any typing so I tried to define it myself:
export type UnmappedObject<T> = T extends ko.ObservableArray<MappedObject<infer U>>
? U[]
: T extends ko.Observable<MappedObject<infer R>>
? R
: T;
// Note: this is not containing ALL the properties exposed
export interface KnockoutMapping {
fromJS: <T>(object: T) => MappedObject<T>;
toJS: <T>(object: T) => UnmappedObject<T>;
}
Note that UnmappedObject<T>
is inspired by Unwrapped<T>
shipped with knockout
, but it doesn't work as I expect because ko.mapping.toJS
will return Unmapped<ObservableArray<MappedObject<Bike>>>
. toJS
should convert both editable object to plain object and array of editable objects to array of plain objects, hence my attempt to guess if T
extends ko.ObservableArray
or ko.Observable
.
What can I change to have properly unwrapping for the fields of each editable object in the array? For clarity, here is what I expect:
const bike = { wheels: 2, isMTB: true, brand: "Cross" }; // Bike
const editBike = ko.mapping.fromJS(bike); // MappedObject<Bike>
const customizedBike = ko.mapping.toJS(editBike); // Bike
const bikes = ko.observableArray<MappedObject<Bike>>();
bikes.push(editBike);
const customizedBikes = ko.mapping.toJS(bikes); // Bike[]
I was close to the final solution, I was missing a few cases here and there:
export type MappedObject<T> = {
[key in keyof T]: ko.Observable<T[key]>;
};
export type UnmappedObject<T> = T extends ko.ObservableArray<MappedObject<infer U>>
? U[]
: T extends ko.Observable<MappedObject<infer V>>
? V
: T extends ko.Computed<MappedObject<infer S>>
? S
: T extends ko.Computed<MappedObject<infer X>[]>
? X[]
: T extends MappedObject<infer W>
? W
: T;
// Note: this is not containing ALL the properties exposed
export interface KnockoutMapping {
fromJS: <T>(object: T) => MappedObject<T>;
toJS: <T>(object: T) => UnmappedObject<T>;
}