Introducing TypeScript 4
Understanding the basic language constructs of TypeScript is very valuable when learning design patterns. You will need to recognize valid TypeScript code and some of its features because it will help you define better typings for objects, as well as help you avoid mistakes. We will strive to provide small but consistent examples and use cases of TypeScript idioms and constructs for completeness.
The basic structure of a TypeScript program consists of statements or expressions. The following is a list of basic types that are partly associated with JavaScript runtime types:
- Primitive types: These are
number
,string
,Boolean
,void
,null
,undefined
,unknown
,never
,unique
,bigint
, andany
values. To define or declare them, you need to write the name of the variable, followed by a semicolon (:) and its type. If you assign the wrong type, then the compiler will throw an error. Here is an example usage of those types (intro.ts
):const one: string = "one"; const two: boolean = false; const three: number = 3; const four: null = null; const five: unknown = 5; const six: any = 6; const seven: unique symbol = Symbol("seven"); let eight: never; // note that const eight: never cannot happen as we cannot instantiate a never
- Enums: They allow us to create named constants, such as the following:
enum Keys { Up, Down, Left, Right, } let up: Keys = Keys.Up;
You can enforce a compiler optimization with enums to make them constant, thus eliminating any unused information:
const enum Bool { True, False, } let truth: Bool = Bool.True;
- Array and tuples: Arrays represent a collection of items of the same type, and they can have a variable size:
const arr: number[] = [1, 2, 3]; // array of numbers of any size
Tuples represent a fixed array, with each element having a defined type:
const tup: [number] = [1]; // tuple with one element of type number
- Classes: These are typical Object-Oriented Programming (OOP) abstractions that allow us to define objects of a specific shape with properties, methods, and visibility modifiers. For example, here is a typical use case of a class:
class User { private name: string; constructor(name: string) { this.name = name; } public getName(): string { return this.name; } } const user = new User("Theo"); console.log(user.getName()); // prints "Theo"
You can also define abstract classes (that is, regular classes) that cannot be instantiated. Instead, they need to be inherited as part of a parent-child relationship:
abstract class BaseApiClient { abstract fetch(req: any): Promise<any>; /* must be implemented in sub-classes*/ } class UsersClient extends BaseApiClient { fetch(req: any): Promise<any> { return Promise.resolve([]); } } const client = new UsersClient(); client.fetch({url: '/users'});
- Interfaces and types: Interfaces are abstractions that let you define the shape of an object and its properties, but without specifying an implementation. For example, we can define a
Comparable
interface like this:interface Comparable<T> { compareTo(o: T): number }
Note that we are not defining an implementation for
compareTo
here, just its type. Interfaces in TypeScript can also have properties:interface AppConfig { paths: { base: string; }; maxRetryCount?: number; }
The question mark (
?
) after the name represents an optional parameter, so it's allowed to create a type with or without it:const appConfig: AppConfig = { paths: { base: '/', } }
Type is a similar concept to interfaces but is a bit more flexible. You can combine a
Type
with anotherType
either as aunion
or as anintersection
type:type A = 'A'; // type is 'A' type B = 'B'; // type is 'B' type C = A & B; /* type is never as there is nothing in common between A and C*/ type D = C | "E"; // type is "E" as C is a never type type E = { name: string; } type F = E & { age: number; } let e: F = { name: "Theo", age: 20 }
Note
As a rule of thumb, you should be declaring interfaces first. However, when you want to combine or create new types on the fly, then you should use types.
There are many other notable features of TypeScript that you will learn about throughout this book. Now, let's move on and learn how to handle input and output.
Working with input and output
Understanding how to read from input and write to output is one of the most fundamental skills of any programming language. Handling input and output operations with TypeScript depends primarily on where you use it. For example, when using TypeScript in a browser environment, you accept input from user interactions, such as when a user clicks on a button and submits a form or when you send an AJAX request to a server.
When using TypeScript in a server, you can read input values from command-line arguments or from the standard input stream (stdin
). Subsequently, we can write values to the output stream, called the standard output stream (stdout
). All these concepts are common to all computer environments.
As an example, let's take a case where we are using TypeScript with Node.js. We can use the following simple program to read from stdin
and write to stdout
:
inputOutput.ts
const stream = process.stdin; setImmediate(function () { stream.push(null); }); stream.pipe(process.stdout);
Then, you can invoke this program from the command line:
echo "Hello" | npm run ts chapters/chapter-1_Getting_Started_With_Typescript_4/inputOutput.ts Hello World
Working with streams exposes a different programming model, called reactive programming, where you are concerned about asynchronous data streams and events. You will learn more about asynchronous communication patterns in Chapter 7, Reactive Programming with TypeScript.
Useful TypeScript 4 features
The latest version of TypeScript (v4.2) offers a great list of features that help developers write type-safe programs and abstractions. For example, with TypeScript 4, we have the following:
- Variadic tuple types: Tuples are interesting data structures as they represent fixed array types, where each element has a specific type. For example, we can model a point in 2D or 3D space as a tuple:
type Point2d = [number, number]; type Point3d = [number, number, number]; const point1: Point2d = [1, 2]; const point2: Point3d = [1, 2, 3];
Before TypeScript 4, you could not pass a variadic type parameter for a tuple as the shape of the tuple had to be defined. Now, let's check out the following code:
type NamedType<T extends unknown[]> = [string, ...T]; type NamedPoint2d = NamedType<Point2d>; const point3: NamedPoint2d = ["Point: (1, 2)", 1, 2];
Here, the type of
NamedPoint2d
is [string
,number
,number
]. With this feature, we may have more compelling reasons to use tuples to model domain primitives. - Labeled tuples: Taking the previous example with the two tuples, we can also add names for each tuple element. This can improve documentation as you can clearly see the corresponding parameter for each item. For example, let's add labels to show the usage of x, y, and z coordinates:
type Point2dL = [x: number, y: number]; type Point3dL = [x: number, y: number, z: number];
Labeled tuples are useful for documentation purposes; so, if you use tuples, you should also provide labels for them.
- Template literal types: TypeScript 4 has added a new literal type for templates that allows us to combine types. This helps when defining new types out of existing ones and you want to avoid repetition. For example, we can model a Deck of Cards type using just two lines of code:
type Suit = `${"Spade" | "Heart" | "Diamond" | "Club"}`; type Rank = `${"2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "Jack" | "Queen" | "King" | "Ace"}` type Deck = `${Rank} of ${Suit}`;
If you inspect the type
of the Deck
declaration, you will see that it enumerates the possible cards of a standard 53 deck of cards: 2 of Spade, 3 of Spade …, Ace of Club.
Now that we've introduced and understood TypeScript 4's features, let's learn how TypeScript and JavaScript are related to each other.