Book Image

Hands-On TypeScript for C# and .NET Core Developers

By : Francesco Abbruzzese
5 (1)
Book Image

Hands-On TypeScript for C# and .NET Core Developers

5 (1)
By: Francesco Abbruzzese

Overview of this book

Writing clean, object-oriented code in JavaScript gets trickier and complex as the size of the project grows. This is where Typescript comes into the picture; it lets you write pure object-oriented code with ease, giving it the upper hand over JavaScript. This book introduces you to basic TypeScript concepts by gradually modifying standard JavaScript code, which makes learning TypeScript easy for C# ASP.NET developers. As you progress through the chapters, you'll cover object programming concepts, such as classes, interfaces, and generics, and understand how they are related to, and similar in, both ES6 and C#. You will also learn how to use bundlers like WebPack to package your code and other resources. The book explains all concepts using practical examples of ASP.NET Core projects, and reusable TypeScript libraries. Finally, you'll explore the features that TypeScript inherits from either ES6 or C#, or both of them, such as Symbols, Iterables, Promises, and Decorators. By the end of the book, you'll be able to apply all TypeScript concepts to understand the Angular framework better, and you'll have become comfortable with the way in which modules, components, and services are defined and used in Angular. You'll also have gained a good understanding of all the features included in the Angular/ASP.NET Core Visual Studio project template.
Table of Contents (16 chapters)

Basic types

TypeScript primitive types obviously include JavaScript primitive types, namely Boolean, number, string, null, and undefined. However, TypeScript adds a few new primitive types and slightly changes the semantics of primitive types. All type declaration examples in the remainder of the chapter may be tested by adding them to our test.ts file.

TypeScript type system

The TypeScript type system is similar to one of the other object-oriented languages, such as C#. There is a root type called any that is similar, but not completely equivalent, to the C# Object. All primitive types are direct descendants of any, while all complex types, such as the Date type, for instance, descend from the object type, which , in turn, is a direct descendant of any:

var myDate: Date = new Date(); //a Date is a complex type
var myString: string = "this is a string"; //string is a simple type
var myNumber: number = 10; //number is a simple type
var myBoolean: boolean = true; //boolean is a simple type

/* Correct all types descend from any */
var x: any = myDate;
x = myString;
x = myNumber;
x = myBoolean;

/* Correct all comlex types descend from object */
var myObject: object = myDate;

/* Wrong! Simple types do not descend from object */

myObject = myString;
myObject = myNumber;
myObject = myBoolean;

The last three statements are wrong since primitive types do not descend from objects; the Visual Studio TypeScript editor should immediately signal the following errors:

Errors are underlined in red, and it is enough to hover the mouse over them to see a detailed error message. So, the C# Object is similar to the TypeScript any and not to the TypeScript object.

any and unknown

While any may be considered the TypeScript root type, the semantics of any are quite different from the usual semantics of other languages' root types. In fact, while other languages' root types allow almost no operations on them, any allows all operations. It is a way to prevent any type check, and was conceived this way to allow compatibility of some code chunks with JavaScript. In fact, once something has been declared as any, it may be processed as it can in simple JavaScript, with no preoccupations about compilation-time type checks.

TypeScript 3.0 also introduces the unknown type, which behaves more like a usual root type since no operations are allowed on a variable declared as unknown, and all values may be assigned to an unknown variable. Thus, from version 3.0 onward, TypeScript has two root types that can be assigned to each other, any and unknown. any disables type checks and allows all operations, while unknown behaves like a usual root type.

Strings, numbers, and Booleans

Strings, numbers, and Booleans have the usual JavaScript semantics; there are no integers or decimals, but all numbers are represented by the number type, which is a 64-bit floating point, and Boolean may have just the true and false values:

var myString: string = "this is a string"; //string is a simple type
var myNumber: number = 10; //number is a 64 bit floating point simple type
var myBoolean: boolean = true; //boolean is a simple type whose only values are: true, false.

The null and undefined subtypes

Like in JavaScript, null and undefined denote both the two types and the only values these types may have:

/* correct null type may assume just the null value */
var myNull: null = null;

/* correct undefined type may assume just the undefined value */
var myUndefined: undefined = undefined;

/* Wrong! */
myNull = 10;
myUndefined = 10;

As a default, null and undefined are subtypes of all types (including all custom types defined by the user), so we may assign them to any variable/property.

As in JavaScript, undefined is the implicit value assigned to any object that was not initialized yet:

/* value is undefined since variable was not initialized */
var notInitialized: number;

However, if the strictNullChecks compiler option is explicitly set to true, null and undefined aren't subtypes of all types anymore, so null and undefined become illegal values for all other types.

Let's add this option to our project's tsconfig.ts:

{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": false,
"strictNullChecks": true,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es5",
"outDir": "wwwroot/js"
},
"include": [
"wwwroot/ts/**/*.ts"
],
"exclude": [
"**/node_modules",
"**/*.spec.ts"
]
}

Then add this code to tests.ts:

/* Wrong! */

var nullCheck: string = null;
var undefinedCheck: string = undefined;

Both declarations will be signaled as wrong, since now null and undefined are illegal values for a string.

If you want a specific string variable to accept null and/or undefined values, you may use another TypeScript feature: union types.

Basics of union types

In order to keep most of the JavaScript flexibility without renouncing compile-time type checks, TypeScript allows the definition of new types as unions of other types. For example:

/* Both statements are correct */
var stringOrNumber: string | number = "Hellow";
stringOrNumber = 10;

A Union Type may assume all values allowed by all its member types, so stringOrNumber may assume both string and number values. Without union types, stringOrNumber should have been declared any, thus renouncing completely any type checks.

We may mix more than two types in a Union Type so for instance, if we set strictNullChecks but we want a specific string variable to also take null and undefined values, we may declare it as follows:

var nullCheck: string|null|undefined = null;
var undefinedCheck: string | null | undefined = undefined;

Now both statements are correct. We may also define aliases for union types:

type NullableString = string | null;

type ExtendedString = string | null | undefined;

var nullCheck: NullableString = null;
var undefinedCheck: ExtendedString = undefined;

This way, we factor out their definitions into a single place and avoid tedious repetitions.

If you set the strictNullChecks options and use Union Types and Type aliases to specify when null and undefined are allowed, you have a better compile-time check and you may avoid hard-to-find bugs.

Union Types may also be defined as unions of either numeric constants or string constants. Here is a string constants example:

type fontStype = "Plain" | "Bold" | "Italic" | "Underlined";

var myFontType: fontStype = "Bold"; //Right

myFontType = "Red"; //Wrong

And here is a numeric constants example:

type dice = 1 | 2 | 3 | 4 | 5 | 6;

var myDice: dice =5; //Right

myDice = 7; //Wrong

void

void is a type that can't assume any value except for null or undefined. It make no sense to declare a variable of type void. This type should be used just to declare a function that must return no value:

function sayHello(): void {
alert("Hello world");
}

If a function whose return value has been declared void actually returns a value along some paths, an error is signaled at compile time:

/* Wrong! */
function sayHelloWrong(): void {
alert("Hello world");
return 1;
}

Also, the converse is true; a function whose return type has been declared, say number, but which doesn't return a value along some paths, will trigger a compile-time error:

/* Wrong! */
function wrongNumer(x: number): number {
if (x > 0) return x;
}

The error may be removed either by adding the missing return statement or by adding void to the return value:

/* Correct! */
function wrongNumer(x: number): number|void {
if (x > 0) return x;
}

never

never is the type whose values never occur! It is the return type of functions that never return, either because of an endless loop or because they always throw an exception:

/* never return type is explicitly declared */
function error(message: string): never {
throw message;
}
/* never return type is inferred by compiler */
function alwaysinError() {
return error("there was an error");
}

You may verify that the compiler automatically infers the never return type of the second function by hovering the mouse over the function name:

Here is an endless loop:

function endlessLoop(): never 
{
while (true)
{
...
...
}
}

Here is a function that may return or may die in an endless loop:

function endlessLoop(x: string): never|number {
while (true) {
if (x == "stop") return 1;
}
}

Enums

TypeScript also provides C#-style enum types:

enum YesNoAnswer { unknown, yes, no};
var myAnswer: YesNoAnswer = YesNoAnswer.unknown;

Like in C#, enum values are translated into integers. As a default, the first value is assigned 0, the second value 1, and so on. However, the default start value may be changed:

enum YesNoAnswer { unknown=1, yes , no}; //yes=2, no=3

You may also provide default integers for all enum values:

enum YesNoAnswer { unknown=1, yes=3 , no=5};

Integers may also be specified through expressions involving the previously defined variables and values of the same enum:

enum YesNoAnswer { unknown=1, yes=unknown+2 , no=yes+2};

The TypeScript compiler generates a variable with the same name as the enum and does something like this:

YesNoAnswer = {};
YesNoAnswer["unknown"] = 1;
YesNoAnswer["yes"] = 3;
YesNoAnswer["no"] = 5;

That's why we may use expressions such as YesNoAnswer.unknown to refer to the enum values.

We may also translate the integer associated with each enum value to the string representing its name:

>YesNoAnswer[3]
>"yes"

This is because the TypeScript compiler generates also something like:

YesNoAnswer[1] = "unknown";
YesNoAnswer[3] = "yes";
YesNoAnswer[5] = "no";

If the enum is defined to be constant, the YesNoAnswer variable and all associated properties are not generated, and each occurrence of YesNoAnswer.unknown, YesNoAnswer.yes, and YesNoAnswer.no in the code is replaced by its associated integer at compile time in the generated JavaScript code:

const enum YesNoAnswer { unknown=1, yes=3 , no=5}; 
var myAnswer: YesNoAnswer = YesNoAnswer.unknown;

/* when the enum is const this is wrong*/
var valueName: string = YesNoAnswer[1];

However, in this case, expressions like YesNoAnswer[3] are not allowed anymore:

Moreover, all integers defining the constant enum values must be constant the compiler may compute at compile time.

Thus, the following is correct:

const enum YesNoAnswer { unknown=1, yes=unknown+2 , no=yes+2};

However, we can't assign the startEnum variable, whose value changes at runtime, to unknown:

var startEnum : number;
...
...
...
const enum YesNoAnswer { unknown=startEnum, yes=unknown+2 , no=yes+2};

Otherwise, we get a compilation error:

Thus, const enum generates less JavaScript code at the price of less flexibility.

enum values may be combined with the bitwise operators &, |, and ~:

const enum YesNoAnswer { unknown = 1, yes = unknown + 2, no = yes + 2 };

var myAnswer: YesNoAnswer = YesNoAnswer.unknown | YesNoAnswer.yes;

Thus, we may define the equivalent of C# bit flags:

const enum TextTransformation {
None = 0,
Bold = 1,
Italic = Bold << 1, //2
Underline = Italic << 1, //4
Overline = Italic << 1, //8
LineThrough = Overline << 1, // 16
HasLine = Underline | Overline | LineThrough
}

function HasBold(x: TextTransformation): boolean {
return (x & TextTransformation.Bold) == TextTransformation.Bold;
}

The HasBold function shows how bit properties may be tested in exactly the same way as in C#.