TypeScript for Beginners

romeopeter

Romeo Agbor Peter

Posted on May 21, 2022

TypeScript for Beginners

A quick view of the everyday types.

TypeScript has been one interesting thing amongst others that have happened to the JavaScript language in recent years. The superset script has indeed made programming in JavaScript not only safer but much more interesting and approachable (especially to developers coming from languages that use compilers).

It goes without saying that the most errors in JavaScript is the type errors. JavaScript inherently does not provide a type checking system for its code. so one big implication is that developers tend to provide a different value when a certain kind of value was expected.

This article will cover JavaScript's common data types and the equivalent ways to describe and typecheck them using TypeScript. It will not be touching the advanced parts (not necessarily) of TypeScript. What this article will show is the building block of TypeScript code.

Let's go...

This article is a simplified version of the everyday types from the TypeScript docs and assumes you know the fundamentals of javascript.

Installing TypeScript.

TypeScript can be installed in 3 ways: through the NPM registry, as a Visual Studio extension, and through the .NET package registry (NuGet).

You can choose any routes you want for the installation. I have NodeJS installed so that's what I'll be using.

Visit here to download TypeScript.

If you want to jump straight into code while following, you can use this TypeScript playground.

Everyday Types

The essence of TypeScript is to provide make for what the language lacked the most –– a static typechecker. A typechecker runs before the actual code runs to ensure all data types are correct (typechecked) and are used as they should in a program.

Annotating Primitives

JavaScript has three basic primitives data types: string,number and boolean. These form the basic level types in the language.

  • String represent text like "Hi Romeo".
  • Number represents a number like 5. Every digit in JavaScript is represented as a number. There aren't special representations like int and float
  • Boolean represents two values, true and false.

Below is the representation of primitive in JavaScript and the equivalent in TypeScript.

// JavaScript
const name = "Romeo";
const age = 23;
const isChristian = true;
Enter fullscreen mode Exit fullscreen mode
// TypeScript
const name: string = "Romeo";
const age: number = 23;
cont isChrisian: boolean = true;
Enter fullscreen mode Exit fullscreen mode

NOTE:

TypeScript is capable of inferring types automatically in your code. For example, the type of a variable is inferred based on the type of its initializer:

let myName = "Alice";
No type annotation needed -- 'myName' inferred as type
'string'

Arrays

This represent types of an array, say a string of names like ["Romeo", "Waan', "Peter"]. you can use the string[] syntax to do so. It works for other types as well, like numbers (number[]).

// JavaScript
const names = ["Romeo", "Waan", "Maxwell", "Peter"];
const numbers = [23, 5.5, 42, 32];
Enter fullscreen mode Exit fullscreen mode
// Typescript
const names: string[] = ["Romeo", "Waan", "Maxwell", "Peter"];
const numbers: number[] = [23, 5.5, 42, 32];
Enter fullscreen mode Exit fullscreen mode

Any

The any type is special to TypeScript and will cause a value not to be validated by TypeScript for its type. In other words, it won't be typechecked.

When a value has the any type, it and its property can be accessed and manipulated as you normally would in JavaScript without it being typechecked. That means it can be assigned to (or from), called as function and its property, which in turn has a type of any, can be accessed.

// Typescript
let someVariable: any = {name: "Romeo"};
someVariable.bar = 100;
someVariable = (n=10) => n;
someVariable();
Enter fullscreen mode Exit fullscreen mode

NOTE:

The any type can be disabled using the noImplicitAny flag. You'll learn more about TypeScript configuration as we proceed.

Functions

Function annotation in TypeScript is of two types: Parameter Type Annotation and Return Type Annotation.

Parameter Type Annotation

When you declare a function you can annotate the parameters to indicate the types of parameters the function expects. parameter type annotation comes after the parameter is declared, like so:

// JavaScript
function greetRomeo(name) {
    console.log("Hey Romeo, I'm " + name);
}
Enter fullscreen mode Exit fullscreen mode
// TypeScript
function greetRomeo(name: string) {
    console.log("Hey Romeo, I'm " + name);
}
Enter fullscreen mode Exit fullscreen mode

Any argument passed to the greetRomeo function will be checked. In the case above, the name parameter expects a string argument, anything else, say a number, will show an error.

NOTE:

TypeScript is strict about the number of parameters passed to a function. Even without annotation, it'll check that you passed in the right number of arguments.

Return Type Annotation

You can annotate the returned value from a function. Anything value return that doesn't match type annotated will be flagged for error.

JavaScript

function getAge() {
    return 23
}
Enter fullscreen mode Exit fullscreen mode
TypeScript

function getAge(): number {
    return 23
}
Enter fullscreen mode Exit fullscreen mode

Typescript can infer the return type based on the value return. Annotating the type value is mostly for documentation purposes.

Personally, I take documentation quite seriously. Fellow developers shouldn't have to peel their scalp to understand what codebase does.

Anonymous function

When a function is passed as a callback (most often anonymous functions), TypeScript can determine how that function will be called thus inferring the parameter type for such functions.

// No type annotations here, but TypeScript can spot the bug
const names = ["Romeo", "Waan", "Peter"];

// Contextual typing for function
names.forEach(function (s) {
  console.log(s.toUppercase()); // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

// Contextual typing also applies to arrow functions
names.forEach((s) => {
  console.log(s.toUppercase()); // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
Enter fullscreen mode Exit fullscreen mode

Notice that the s parameter for both functions –– pure and arrow –– is not annotated, yet TypeScript could infer the correct type because it knows how the anonymous function will work on an array in that context. This is called contextual typing because the context where the function is used is known to TypeScript thus it (TypeScript) can infer the type the function should have.

Object types

Besides primitives, a common form of data type you deal with as a javascript developer is an Object; this is any JavaScript data type with properties and values.

To define an object type, you list the properties and their types. For instance, here's a function that takes an object as a parameter.

// JavaScript

function romeosProfile(profile){
    if (typeof profile === "Object") {
        console.log("Romeo's age is " + profile.name);
        console.log("Romeo's height is " + profile.height);
    }
}
romeosProfile({name: "Romeo", age: 23});
Enter fullscreen mode Exit fullscreen mode
// TypeScript

// The parameter's type annotation is an object type
function romeosProfile(pt: {name: string, age: string}){
    console.log("Romeo's age is " + pt.name);
    console.log("Romeo's height is " + pt.height);
}
romeosProfile({name: "Romeo", age: 23});
Enter fullscreen mode Exit fullscreen mode

The parameter of the function is annotated as an object type. the further annotation of the object type itself is optional which, if done so, will have a type of any.

Optional properties

Object types can specify some optional properties by appending a ? after the property name.

function romeosProfile(obj: {name: string, age?: number}) {
  // ...
}

// Both OK
printName({ name: "Romeo" });
printName({ name: "Romeo",  age: 23 });
Enter fullscreen mode Exit fullscreen mode

Say you didn't provide an argument for the optional parameter and you accessed it, it will return undefined because the property doesn't exist. When reading from an optional property, make sure to check that it's not undefined.

function romeosProfile(pt: {name: string, age?: number}) {

  // Error - might crash if 'pt.age' wasn't provided!
  console.log(pt.age);

  if (pt.age !== undefined) {
    // OK
    console.log(pt.age);
  }

  // A safe alternative using modern JavaScript syntax:
  console.log(pt.age?);
}
Enter fullscreen mode Exit fullscreen mode

Combining Types in TypeScript

The type system in TypeScript allows you to combine and form new types from existing ones.

Union Type

A union type is a type formed by combing two or more other types that represent values of any of the existing types. the combined types are referred to as the union's members.

Here is a function that accepts a string and a number as its parameter.

function printAge(age: number | string) {
    console.log("i'm " + " years old");
}
printAge(23) // I'm 23 years old
printAge("23") // I'm 23 years old
printAge({age: 23}) // Error
Enter fullscreen mode Exit fullscreen mode

Working with Union Type

TypeScript will only allow an operation if it's valid (intersection of type property) for either of the union members. For instance, you can't perform a toUpperCase operation on a string | number union and that's because the operation is only valid for value with a type of string but the type value could be a number.

To get around a union type whose property may not intersect, you use Narrowing –– to "... deduce a more specific type for a value based on the structure of the code."

For instance, using a conditional to let TypeScript know that an operation for a certain type.

function printAge (age: number | string) {
    if (typeof age === "string") {
        console.log(age.toUpperCase());
    } else {
        // age is type of 'number'
        console.log(age)
    }
}
Enter fullscreen mode Exit fullscreen mode

or if member of union type is an array:

function meetFriends(x: string[] | string) {
    if (Array.isArray(x)) {
        console.log("Hello, " +  x.join(" and ") + ". Nice to meet you all!")
    } else {
        // is a string and not an array
        console.log("Hello " + x);
    }
}
Enter fullscreen mode Exit fullscreen mode

As indicated earlier, members of a union type whose value share (intersect) properties don't need narrowing:

function getFirstTime(x: number[] | string) {
    return x.slice(0, 3);
}
Enter fullscreen mode Exit fullscreen mode

Type Aliases

Type aliases allow you to name a type and use it more than once by only referring to it by name. It is a name for any type.

// Object aliase type
type Profile = {
    name: string,
    age: number
}

// Union Type aliase type
type ID = number | string;
Enter fullscreen mode Exit fullscreen mode

With the Profile type alias above, you can pass it as a parameter type for a function by simply referring to the name (Profile).

// Type Profile creates a type structure to be used as parameter type
function getProfile(pt: Profile) {
    console.log("Name: " + pt.name);
    console.log("Age: " + pt.age);
}
getProfile({name: "Romeo", age: 23});
Enter fullscreen mode Exit fullscreen mode

Interfaces

Interfaces are similar to types aliases, and the two can be used interchangeably to create a named type for an object. The only difference is that a type alias can't be added new fields when once created, compared to an interface that is opened to be added more fields.

// Object interface
interface Profile = {
    name: string,
    age: number
}

// Valid and will be combined with above interface
interface Profile = {
    eyeColor: string
}

// Type Profile creates a type structure to be used as parameter type
function getProfile(pt: Profile) {
    console.log("Name: " + pt.name);
    console.log("Age: " + pt.age);
}
getProfile({name: "Romeo", age: 23});
Enter fullscreen mode Exit fullscreen mode

It is generally preferred to use interface when creating a named type for objects, just in case you'd want to add to the existing fields.

interface Profile = {
    name: string,
    age: number,
    eyeColor: string
}


// Object takes the structure of defined interface using Type Assertion
const RomeosProfile: <Profile> {
    name: "Romeo",
    age: 23,
    eyeColor: "black"
}
Enter fullscreen mode Exit fullscreen mode

Type Assertions

Type assertion allows you to declare (assert) a type on a variable so that the compiler won't infer it at runtime. This is because you, as the programmer, could have more information about the type of value that TypeScript can't or at least, it'll infer something not quite right.

Type assertion is similar to typecasting in other languages like C++, C# and java except that there is no runtime effect (all assertions are removed by the compiler) in TypeScript.

for instance, if you're accessing the DOM using document.getElementbyId, TypeScript understands that it'll return an HTMLElement, but you might know specifically that it'll be an HTMLCanvasElement.

You can use a type assertion to specify that:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
Enter fullscreen mode Exit fullscreen mode

You can use an angle-bracket to achieve the same effect.

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
Enter fullscreen mode Exit fullscreen mode

Another instance is asserting an object, similar to the one you saw earlier.

interface Creator { 
    name: string; 
    code: number; 
}

// Using the `as` keyword
let person = {} as Creator; // Ok
person.name = "Romeo";
person.age = 23;


// Using angle-bracket
let person = <Creator> {}; // ok
person.name = "Romeo";
person.age = 23;
Enter fullscreen mode Exit fullscreen mode

NOTE:

When dealing with TypeScript in React (.tsx format), only the as keyword is allowed because using angle-brackets in JSX will create a conflict in the React code.

Literal Types

With literal types, you can create and refer to specific strings and numbers in type positions.

For instance, a variable with a specific string type:

let x: "hello" = "hello";
x = "Howdy"; // Type '"howdy"' is not assignable to type '"hello"'.
Enter fullscreen mode Exit fullscreen mode

When combined to form a union, literals can be used to create complex and useful type structures in type positions.

for instance, a function with a second parameter that only accepts certain values.

function creator(age: number, name: "Romeo" | "Waan" | "Peter"): void {
    console.log(alignment);
};

creator(23,"middle"); // Argument of type '"middle"' is not assignable to parameter of type '"Romeo" | "Waan" | "Peter

creator(23, "Waan") // No error, will log `Waan`
Enter fullscreen mode Exit fullscreen mode

Also, a function that can only return numerical literals:

function compare(a: string, b: string): 1 | -1 | 0 {
    return a === b ? 0 : a < b ? 1 : -1;
}

compare("23", "34"); // No error, will log 1;
Enter fullscreen mode Exit fullscreen mode

Literal types can be combined with non-literal types too.

interface Option {
    width: number;
}

function configure(x: Options | "auto") {
  // ...
}

configure({ width: 100 }); // No error
configure("auto"); // No error
configure("automatic"); // Error: Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
Enter fullscreen mode Exit fullscreen mode

Although we didn't use it, the boolean (true and false) literal type can be used to achieve a similar concept as the ones above.

NOTE:

The boolean type is actually an alias that represents the union of true and false.

Alright, that's about it for the everyday types you'll encounter while using or reading TypeScript code. Before I round up things, let's look into configuring TypeScript using the tsconfig.json. file

TS Config

The TypeScript configuration file is tsconfig.json which sits at the root of the project. It's automatically created when the TypeScript is first initialized. The file specifies the root files and compiler options for the project.

Using the .tsconfig to compile project.

There are two ways you can do this:

  • By invoking the tsc CLI command with no input files, in which case the compiler uses the tsconfig.json (starting from the current directory and going the directory chain) file to look for the project file to compile.
  • By invoking the tsc CLI command with a --project or -p option that specifies the directory to the .tsconfig.json file containing the configuration.

When input files are specified on the command line, the ones specified in the tsconfig.json files are ignored.

Here is a reference to the configuration options you can make as fitting to a project.

Conclusion.

The basis of this article is to get beginners started with building blocks of TypeScript code. The TypeScript documentation answers questions this article fails to provide. Do check it out.

Let's connect on Twitter, I hear that my tweets are quite intriguing.

Cheers!

💖 💪 🙅 🚩
romeopeter
Romeo Agbor Peter

Posted on May 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related