can-type
Object
Overview
Use can-type to define rules around types to handle type checking and type conversion. Works well with can-define, can-observable-object, and can-stache-element.
can-type specifies the following type functions:
type.maybe
Use maybe to specify a TypeObject which will accept a value that is a member of the provided type, or is undefined
or null
.
import { Reflect, type } from "can";
const NumberType = type.maybe(Number);
let val = Reflect.convert(42, NumberType);
console.log(val); // -> 42
val = Reflect.convert(null, NumberType);
console.log(val); // -> null
val = Reflect.convert(undefined, NumberType);
console.log(val); // -> undefined
Reflect.convert("hello world", NumberType); // throws!
type.convert
Use convert to define a TypeObject which will coerce the value to the provided type.
import { Reflect, type } from "can";
const NumberType = type.convert(Number);
let val = Reflect.convert("42", NumberType);
console.log(val); // -> 42
type.maybeConvert
Use maybeConvert to define a TypeObject which will coerce the value to the type, but also accept values of null
and undefined
.
import { Reflect, type } from "can";
const DateType = type.maybeConvert(Date);
const date = new Date();
let val = Reflect.convert(date, DateType);
console.log(val); // -> date
val = Reflect.convert(null, DateType);
console.log(val); // -> null
val = Reflect.convert(undefined, DateType);
console.log(val); // -> undefined
val = Reflect.convert("12/04/1433", DateType);
console.log(val); // -> Date{12/04/1433}
type.check
Use check to specify a strongly typed TypeObject that verifies the value passed is of the same type.
import { Reflect, type } from "can";
const StringType = type.check(String);
let val = Reflect.convert("hello world", StringType);
console.log(val); // -> hello world
Reflect.convert(42, StringType); // throws!
Creating Models and ViewModels
can-type is useful for creating typed properties in can-observable-object. You might want to use stricter type checking for some properties or classes and looser type checking for others. The following creates properties with various properties and type methods:
import { ObservableObject, type } from "can";
class Person extends ObservableObject {
static props = {
first: type.check(String), // type checking is the default behavior
last: type.maybe(String),
age: type.convert(Number),
birthday: type.maybeConvert(Date)
};
}
let fib = new Person({
first: "Fibonacci",
last: null,
age: "80",
birthday: undefined
});
console.log(fib); // ->Person{ ... }
Note: as mentioned in the comment above, type checking is the default behavior of can-observable-object, so
first: type.check(String)
could be written asfirst: String
.
When creating models with can-rest-model you might want to be loose in the typing of properties, especially when working with external services you do not have control over.
On the other hand, when creating ViewModels for components, such as with can-stache-element you might want to be stricter about how properties are passed, to prevent mistakes.
import { StacheElement, type } from "can";
class Progress extends StacheElement {
static props = {
value: {
type: type.check(Number),
default: 0
},
max: {
type: type.check(Number),
default: 100
},
get width() {
let w = (this.value / this.max) * 100;
return w + '%';
}
};
static view = `
<div style="background: black;">
<span style="background: salmon; display: inline-block; width: {{width}}"> </span>
</div>
`;
}
customElements.define("custom-progress-bar", Progress);
let progress = new Progress();
progress.value = 34;
document.body.append(progress);
function increment() {
setTimeout(() => {
if(progress.value < 100) {
progress.value++;
increment();
}
}, 500);
}
increment();
Note: Having both
type: type.check(Number)
anddefault: 0
in the same definition is redundant. Usingdefault: 0
will automatically set up type checking. It is shown above for clarity.
See can-stache-element and can-observable-object for more on these APIs.
How it works
The can-type
methods work by creating functions that are compatible with canReflect.convert.
These functions have a can.new Symbol that points to a function that is responsible for creating an instance of the type. The following is an overview of how this function works:
1. Determine if value is already the correct type
- Maybe types (
type.maybe
,type.maybeConvert
) will returntrue
if the value isnull
orundefined
. - Common primitive types (
Number
,String
,Boolean
) will returntrue
if typeof returns the correct result. - Other types will return
true
if the value is an instanceof the type. - TypeObjects (or anything with a
can.isMember
Symbol) will returntrue
if thecan.isMember
function returnstrue
. - Otherwise, the value is not the correct type.
2. Handle values of another type
If the value is not the correct type:
type.maybe
andtype.check
will throw an error.type.convert
andtype.maybeConvert
will convert the value using convert.
Applying multiple type functions
The type functions check, convert, maybe, and maybeConvert all return a TypeObject. Since they also can take a TypeObject as an argument, this means you can apply multiple type functions.
For example, using convert and maybe is equivalent to using maybeConvert:
import { Reflect, type } from "can";
const MaybeConvertString = type.convert(type.maybe(String));
console.log(2, Reflect.convert(2, MaybeConvertString)); // "2"
console.log(null, Reflect.convert(2, MaybeConvertString)); // null
Another example is taking a strict type and making it a converter type by wrapping with convert:
import { Reflect, can } from "can";
const StrictString = type.check(String);
const NonStrictString = type.convert(StrictString);
console.log("Converting: ", Reflect.convert(5, NonStrictString)); // "5"
This works because the type functions all keep a reference to the underlying type and simply toggle the strictness of the newly created TypeObject. When can.new is called the strictness is checked.