Skip to content

Instantly share code, notes, and snippets.

@JSuder-xx
Created February 18, 2020 00:59
Show Gist options
  • Save JSuder-xx/656f041cb866e700a5c3337771d2f3a7 to your computer and use it in GitHub Desktop.
Save JSuder-xx/656f041cb866e700a5c3337771d2f3a7 to your computer and use it in GitHub Desktop.
Inferring a strong static type signature from the CanJS dynamic view model API.
/**
* Demonstrate inferring a strong type signature from the CanJS dynamic view model API.
*/
module ReadMe {}
module ViewModelTypes {
/** Definition of a "class". */
export type Definition = { attributes: any }
}
module ViewModelTypeInference {
/** Convert a string name for a type to a type. */
type StringNameToType<stringName> =
stringName extends "string" ? string
: stringName extends "number" ? number
: stringName extends "boolean" ? boolean
: stringName extends "Date" ? Date
: any;
/** Return the keys of Attributes which map to the given types. */
type KeysMappingTo<attributes, type> = {
[propertyName in keyof attributes]: StringNameToType<attributes[propertyName]> extends type ? propertyName : never
}[keyof attributes]
/** The type signature of an attribute method for a given data type. */
type AttributeMethodType<attributes, type> = {
(properyName: KeysMappingTo<attributes, type>, value: type): void;
(properyName: KeysMappingTo<attributes, type>): type;
}
/** Determine if there is an .attr method overload appropriate for the given type and if so produce it else empty object */
type AttributeMethodOverload<attributes, type> =
KeysMappingTo<attributes, type> extends never
? {}
: { attr: AttributeMethodType<attributes, type> }
/** Get the intersection of attribute method overloads */
type AttrMethodsOfAttributes<attributes> =
AttributeMethodOverload<attributes, string>
& AttributeMethodOverload<attributes, number>
& AttributeMethodOverload<attributes, boolean>
& AttributeMethodOverload<attributes, Date>
/** Generate a view model instance type for the given definition. */
type ViewModelInstance<definition extends ViewModelTypes.Definition> =
AttrMethodsOfAttributes<definition["attributes"]>;
/** Generate the JSON that should be injected into a view model for the attributes. */
type JsonFromAttributes<attributes> = {
[PropertyName in keyof attributes]: StringNameToType<attributes[PropertyName]>
}
/** Generate the JSON that should be injected into a view model based on the definition. */
type JsonFromDefinition<definition extends ViewModelTypes.Definition> = JsonFromAttributes<definition["attributes"]>;
/** Produce the ViewModelClass type from the type definition. */
export type ViewModelClass<definition extends ViewModelTypes.Definition> = { new(args: JsonFromDefinition<definition>): ViewModelInstance<definition> }
}
module ViewModel {
/** Now pretend this is the CanJS ViewModel class function. */
export function ViewModelClass<definition extends ViewModelTypes.Definition>(name: string, definition: definition): ViewModelTypeInference.ViewModelClass<definition> {
throw new Error("Not implemented");
}
}
module Test {
const Person = ViewModel.ViewModelClass(
"Person",
{
attributes: {
firstName: "string" as const,
lastName: "string" as const,
weight: "number" as const,
isCool: "boolean" as const
}
});
const person = new Person({
firstName: "John",
lastName: "Suder",
weight: 175,
isCool: false
});
person.attr("firstName", "Jack");
// person.attr("firstNam", "Jon"); // Uncomment to see error because there is no property with that name
person.attr("weight", 10);
// person.attr("weight", "100"); // Uncomment to see error because property name/type mismatch.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment