Tag types
See original GitHub issueProblem
- There is no straight way to encode a predicate (some checking) about a value in its type.
Details
There are situations when a value has to pass some sort of check/validation prior to being used. For example: a min/max functions can only operate on a non-empty array so there must be a check if a given array has any elements. If we pass a plain array that might as well be empty, then we need to account for such case inside the min/max functions, by doing one of the following:
- crashing
- returning undefined
- returning a given default value
This way the calling side has to deal with the consequences of min/max being called yet not being able to deliver.
function min<a>(values: a[], isGreater: (one: a, another: a) => boolean) : a {
if (values.length < 1) { throw Error('Array is empty.'); }
// ...
}
var found;
try {
found = min([]);
} catch (e) {
found = undefined;
}
Solution
A better idea is to leverage the type system to rule out a possibility of the min function being called with an empty array. In order to do so we might consider so called tag types.
A tag type is a qualifier type that indicates that some predicate about a value it is associated with holds true.
const enum AsNonEmpty {} // a tag type that encodes that a check for being non-empty was passed
function min<a>(values: a[] & AsNonEmpty) : a {
// only values that are tagged with AsNonEmpty can be passed to this function
// the type system is responsible for enforcing this constraint
// a guarantee by the type system that only non-empty array will be passed makes
// implementation of this function simpler because only one main case needs be considered
// leaving empty-case situations outside of this function
}
min([]); // <-- compile error, tag is missing, the argument is not known to be non-empty
min([1, 2]); // <-- compile error, tag is missing again, the argument is not know to be non-empty
it’s up to the developer in what circumstances an array gets its AsNonEmpty tag, which can be something like:
// guarntee is given by the server, so we always trust the data that comes from it
interface GuaranteedByServer {
values: number[] & AsNonEmpty;
}
Also tags can be assigned at runtime:
function asNonEmpty(values: a[]) : (a[] & AsNonEmpty) | void {
return values.length > 0 ? <a[] & AsNonEmpty> : undefined;
}
function isVoid<a>(value: a | void) : value is void {
return value == null;
}
var values = asNonEmpty(Math.random() > 0.5 ? [1, 2, 3] : []);
if (isVoid(values)) {
// there are no elements in the array, so we can't call min
var niceTry = min(values); // <-- compile error;
} else {
var found = min(values); // <-- ok, tag is there, safe to call
}
As was shown in the current version (1.6) an empty const enum type can be used as a marker type (AsNonEmpty in the above example), because
- enums might not have any members and yet be different from the empty type
- enums are branded (not assignable to one another)
However enums have their limitations:
- enum is assignable by numbers
- enum cannot hold a type parameter
- enum cannot have members
A few more examples of what tag type can encode:
string & AsTrimmed & AsLowerCased & AsAtLeast3CharLong
number & AsNonNegative & AsEven
date & AsInWinter & AsFirstDayOfMonth
Custom types can also be augmented with tags. This is especially useful when the types are defined outside of the project and developers can’t alter them.
User & AsHavingClearance
ALSO NOTE: In a way tag types are similar to boolean properties (flags), BUT they get type-erased and carry no rutime overhead whatsoever being a good example of a zero-cost abstraction.
UPDATED:
Also tag types can be used as units of measure in a way:
string & AsEmail
,string & AsFirstName
:
var email = <string & AsEmail> 'aleksey.bykov@yahoo.com';
var firstName = <string & AsFirstName> 'Aleksey';
firstName = email; // <-- compile error
number & In<Mhz>
,number & In<Px>
:
var freq = <number & In<Mhz>> 12000;
var width = <number & In<Px>> 768;
freq = width; // <-- compile error
function divide<a, b>(left: number & In<a>, right: number & In<b> & AsNonZero) : number & In<Ratio<a, b>> {
return <number & In<Ratio<a, b>>> left / right;
}
var ratio = divide(freq, width); // <-- number & In<Ratio<Mhz, Px>>
Issue Analytics
- State:
- Created 8 years ago
- Reactions:46
- Comments:55 (8 by maintainers)
Top GitHub Comments
@agos the problem with that solution is that it’s not type safe. Symbols are the only nominal type, so you need a solution that uses them. That much is clear.
Here’s the sample adapted to be type-safe:
We have talked about this one a few times recently. @weswigham volunteered to write up a proposal.