Created
February 18, 2020 00:59
-
-
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.
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
/** | |
* 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