|
|
@@ -0,0 +1,199 @@ |
|
|
/** |
|
|
* _Explicitly declare modifications_ to an existing object type via a fluent interface which yields a mapping function |
|
|
* from the original data type to the type derived from the modification commands. |
|
|
* |
|
|
* The value over authoring a simple mapping function directly |
|
|
* * harder to make a mistake due to the explicit nature |
|
|
* * easier to read. |
|
|
* |
|
|
* This is _not_ production quality code but rather a _proof of concept_ gist for applying conditional/mapped |
|
|
* types to mapper generation. A real-world usage might involve also generating the type-guard function |
|
|
* and integrating the entire thing with some kind of Document Database migration system. |
|
|
* |
|
|
* **NOTE**: This code file can be copy/pasted into the [TypeScript Playground](http://www.typescriptlang.org/play/) |
|
|
* |
|
|
* **NOTE**: Type arguments are named descriptively using camelcase; just like any other argument/variable. |
|
|
**/ |
|
|
module FluentMapperBuilder { |
|
|
/** Produce a new type by removing a set of properties from a given an object type. */ |
|
|
export type RemoveProperties<originalObject extends {}, propertiesToRemove extends (keyof originalObject)[]> = |
|
|
propertiesToRemove extends (infer propertiesToRemoveUnion)[] |
|
|
? Pick<originalObject, Exclude<keyof originalObject, propertiesToRemoveUnion>> |
|
|
: never; |
|
|
|
|
|
/** Produce a new object type changing the type of one of the properties. */ |
|
|
export type ChangePropertyType<originalObject extends {}, nameOfPropertyToChange extends keyof originalObject, newPropertyType> = |
|
|
{ |
|
|
[currentProperty in keyof originalObject]: currentProperty extends nameOfPropertyToChange |
|
|
? newPropertyType |
|
|
: originalObject[currentProperty] |
|
|
}; |
|
|
|
|
|
/** Produce a new object type by renaming a property. */ |
|
|
export type RenameProperty<originalObject, originalPropertyName extends keyof originalObject, newPropertyName extends string | symbol> = |
|
|
// Drop the original name |
|
|
Pick<originalObject, Exclude<keyof originalObject, originalPropertyName>> |
|
|
& // Add the new property name with the original type |
|
|
Record<newPropertyName, originalObject[originalPropertyName]>; |
|
|
|
|
|
/** A fluent builder for strongly typed object to object transforms. */ |
|
|
export interface IObjectTypeMapperBuilder<originalObjectInPipeline, transformThisObject> { |
|
|
removeProperties<removeProperties extends (keyof transformThisObject)[]>( |
|
|
removeSpecification: removeProperties |
|
|
): IObjectTypeMapperBuilder<originalObjectInPipeline, RemoveProperties<transformThisObject, removeProperties>>; |
|
|
|
|
|
addProperty<newPropertyName extends string>(newPropertyName: Exclude<newPropertyName, keyof transformThisObject>): { |
|
|
withDefaultValue<newPropertyType>(defaultValue: newPropertyType): IObjectTypeMapperBuilder<originalObjectInPipeline, transformThisObject & Record<newPropertyName, newPropertyType>>; |
|
|
}; |
|
|
|
|
|
renameProperty<existingPropertyName extends keyof transformThisObject, newPropertyName extends string>( |
|
|
existingPropertyName: existingPropertyName, |
|
|
newPropertyName: newPropertyName |
|
|
): IObjectTypeMapperBuilder<originalObjectInPipeline, RenameProperty<transformThisObject, existingPropertyName, newPropertyName>>; |
|
|
|
|
|
mapProperty<newType, propertyName extends keyof transformThisObject>( |
|
|
propertyName: propertyName, |
|
|
map: (val: transformThisObject[propertyName]) => newType |
|
|
): IObjectTypeMapperBuilder<originalObjectInPipeline, ChangePropertyType<transformThisObject, propertyName, newType>>; |
|
|
|
|
|
getMapper(): (original: originalObjectInPipeline) => transformThisObject; |
|
|
} |
|
|
|
|
|
// Interface is exported but the class is not for encapsulation. |
|
|
class ObjectTypeMapperBuilder< |
|
|
originalObjectInPipeline extends {}, |
|
|
transformThisObject extends {} |
|
|
> implements IObjectTypeMapperBuilder<originalObjectInPipeline, transformThisObject> { |
|
|
|
|
|
constructor(private readonly _transform: (original: originalObjectInPipeline) => transformThisObject) { } |
|
|
|
|
|
public addProperty<newPropertyName extends string>(newPropertyName: Exclude<newPropertyName, keyof transformThisObject>) { |
|
|
const { _transform } = this; |
|
|
return { withDefaultValue } |
|
|
|
|
|
function withDefaultValue<newPropertyType>(defaultValue: newPropertyType) { |
|
|
return new ObjectTypeMapperBuilder((originalObject: originalObjectInPipeline): transformThisObject & Record<newPropertyName, newPropertyType> => { |
|
|
const thisObject = _transform(originalObject); |
|
|
return { |
|
|
...thisObject |
|
|
, [newPropertyName]: defaultValue |
|
|
} as any; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
public removeProperties<removeProperties extends (keyof transformThisObject)[]>( |
|
|
removeProperties: removeProperties |
|
|
): IObjectTypeMapperBuilder<originalObjectInPipeline, RemoveProperties<transformThisObject, removeProperties>> { |
|
|
return new ObjectTypeMapperBuilder((originalObject: originalObjectInPipeline): RemoveProperties<transformThisObject, removeProperties> => { |
|
|
const thisObject = this._transform(originalObject); |
|
|
const newObj: { [index: string]: any } = {}; |
|
|
Object.keys(thisObject).forEach((property) => { |
|
|
if (removeProperties.indexOf(property as any) === -1) |
|
|
newObj[property] = (thisObject as any)[property]; |
|
|
}); |
|
|
return newObj as any; |
|
|
}); |
|
|
} |
|
|
|
|
|
public renameProperty<existingPropertyName extends keyof transformThisObject, newPropertyName extends string>( |
|
|
existingPropertyName: existingPropertyName, |
|
|
newPropertyName: newPropertyName |
|
|
): IObjectTypeMapperBuilder<originalObjectInPipeline, RenameProperty<transformThisObject, existingPropertyName, newPropertyName>> { |
|
|
return new ObjectTypeMapperBuilder((originalObject: originalObjectInPipeline): RenameProperty<transformThisObject, existingPropertyName, newPropertyName> => { |
|
|
const thisObject = this._transform(originalObject); |
|
|
const newObj: { [index: string]: any } = {}; |
|
|
Object.keys(thisObject).forEach((property) => { |
|
|
newObj[property === existingPropertyName ? newPropertyName : property] = (thisObject as any)[property]; |
|
|
}); |
|
|
return newObj as any; |
|
|
}); |
|
|
} |
|
|
|
|
|
public mapProperty<newType, propertyName extends keyof transformThisObject>( |
|
|
propertyName: propertyName, |
|
|
transform: (val: transformThisObject[propertyName]) => newType |
|
|
): IObjectTypeMapperBuilder<originalObjectInPipeline, ChangePropertyType<transformThisObject, propertyName, newType>> { |
|
|
return new ObjectTypeMapperBuilder((originalObject: originalObjectInPipeline): ChangePropertyType<transformThisObject, propertyName, newType> => { |
|
|
const thisObject = this._transform(originalObject); |
|
|
return { |
|
|
...thisObject |
|
|
, [propertyName]: transform(thisObject[propertyName]) |
|
|
} as any; |
|
|
}); |
|
|
} |
|
|
|
|
|
public getMapper(): (original: originalObjectInPipeline) => transformThisObject { |
|
|
return this._transform; |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
/** Start building a mapper that maps from this type. */ |
|
|
export const from = <T>(): IObjectTypeMapperBuilder<T, T> => |
|
|
new ObjectTypeMapperBuilder<T, T>(it => it); |
|
|
} |
|
|
|
|
|
//---------------------------------------------------------------------------- |
|
|
// Example Usage |
|
|
//---------------------------------------------------------------------------- |
|
|
module ExampleTypesAndMappers { |
|
|
// Starting data type |
|
|
export type Person = { |
|
|
fName: string; |
|
|
lName: string; |
|
|
dob: Date; |
|
|
selfEsteem: "Low" | "Medium" | "High"; |
|
|
flurbNibble: number; |
|
|
} |
|
|
|
|
|
// An enumerated string data type |
|
|
export type Gender = "Unknown" | "Female" | "Male" | "Other"; |
|
|
|
|
|
// mapper |
|
|
const dateToDateJson = (date: Date) => ({ |
|
|
year: date.getFullYear() |
|
|
, month: date.getMonth() |
|
|
, date: date.getDate() |
|
|
}); |
|
|
|
|
|
// declare a mapper function using fluent interface |
|
|
export const mapToPersonV2 = FluentMapperBuilder.from<Person>() |
|
|
.addProperty("weightInLbs").withDefaultValue<number | null>(null) |
|
|
.addProperty("gender").withDefaultValue<Gender>("Unknown") |
|
|
// Uncomment the line below to watch the compiler complain because gender cannot be added again |
|
|
//.addProperty("gender").withDefaultValue("Male") |
|
|
.mapProperty("dob", dateToDateJson) |
|
|
.renameProperty("lName", "lastName") |
|
|
.renameProperty("fName", "firstName") |
|
|
.removeProperties(["selfEsteem", "flurbNibble"]) |
|
|
.getMapper(); |
|
|
|
|
|
// get the result type from the mapper |
|
|
export type PersonV2 = ReturnType<typeof mapToPersonV2>; |
|
|
} |
|
|
|
|
|
|
|
|
const originalJim: ExampleTypesAndMappers.Person = { |
|
|
fName: "Jim" |
|
|
, lName: "Smith" |
|
|
, dob: new Date(1980, 1, 1) |
|
|
, selfEsteem: "Low" |
|
|
, flurbNibble: 42 |
|
|
}; |
|
|
|
|
|
const jimV2 = ExampleTypesAndMappers.mapToPersonV2(originalJim); |
|
|
|
|
|
// Hover over a field to inspect type/structure. |
|
|
[ |
|
|
jimV2.firstName |
|
|
, jimV2.lastName |
|
|
, jimV2.gender |
|
|
, jimV2.dob |
|
|
, jimV2.weightInLbs |
|
|
//, jimV2.flurbNibble |
|
|
//, jimV2.selfEsteem |
|
|
]; |
|
|
console.log("Original", originalJim); |
|
|
console.log("Mapped", jimV2); |
|
|
alert("Check the Dev console for output."); |