Using different types with union types
The first feature that we are going to look at is one of my favorites, that is, the ability to use union types. These types are used when a function expects a single parameter to be one type or another. Suppose, for instance, that we have a validation routine that needs to check whether a value is in a particular range and this validation could receive the value either from a textbox as a string value, or as a number value from a calculation. As each of the techniques for solving this issue have a lot in common, we'll start off with a simple class that allows us to specify the minimum and maximum values that form our range and a function to actually perform the validation, as follows:
class RangeValidationBase {
constructor(private start : number, private end : number) { }
protected RangeCheck(value : number) : boolean {
return value >= this.start && value <= this.end;
}
protected GetNumber(value : string) : number {
return new Number(value).valueOf();
}
}
If you haven't seen a constructor that looks like that before, that's the equivalent of writing the following:
private start : number = 0;
private end : number = 0;
constructor(start : number, end : number) {
this.start = start;
this.end = end;
}
If you need to check your parameters or manipulate them in some way, you should use this expanded format of parameters. If you are simply assigning the values to private fields, then the first format is a very elegant way to do this and saves cluttering up your code.
There are a few ways that we could solve the problem of ensuring we only perform our validation using string or number. The first way we could solve this problem would be by providing two separate methods that accept the relevant type, as follows:
class SeparateTypeRangeValidation extends RangeValidationBase {
IsInRangeString(value : string) : boolean {
return this.RangeCheck(this.GetNumber(value));
}
IsInRangeNumber(value : number) : boolean {
return this.RangeCheck(value);
}
}
While this technique would work, it's not very elegant and it certainly doesn't take advantage of the power of TypeScript. The second technique that we could use is to allow us to pass in the value without constraining it, as follows:
class AnyRangeValidation extends RangeValidationBase {
IsInRange(value : any) : boolean {
if (typeof value === "number") {
return this.RangeCheck(value);
} else if (typeof value === "string") {
return this.RangeCheck(this.GetNumber(value));
}
return false;
}
}
That's definitely an improvement over our original implementation because we have settled on one signature for our function, which means that calling the code is a lot more consistent. Unfortunately, we can still pass an invalid type into the method, so if we passed boolean in, for instance, this code would compile successfully but it would fail at runtime.
If we want to constrain our validation so that it only accepts strings or numbers, then we can use a union type. It doesn't differ much from the last implementation but it does give us the compile time type safety that we're after, as follows:
class UnionRangeValidation extends RangeValidationBase {
IsInRange(value : string | number) : boolean {
if (typeof value === "number") {
return this.RangeCheck(value);
}
return this.RangeCheck(this.GetNumber(value));
}
}
The signature that identifies the type constraints as being a union is type | type in the function name. This tells the compiler (and us) what the valid types are for this method. As we have constrained the input to be number or string, once we have ruled out that the type is not number, we don't need to check typeof to see whether it's a string so we have simplified the code even further.
We can go further than this with union types. In TypeScript, we have two special types, null and undefined. These types can be assigned to anything unless we compile our code with the –strictNullChecks option or strictNullChecks = true if we're setting this as a flag in our tsconfig.json file. I like to set this value so that my code only handles null cases where it should, which is a great way to guard against side effects creeping in just because a function receives a null value. If we want to allow null (or undefined), we simply need to add these as a union type.