TypeScript’s docs define typescript as a strongly typed programming language based on JavaScript. TypeScript was built and is maintained by Microsoft and their tagline for the language is “JavaScript that scales”. As you can probably infer from the tagline, TypeScript is built on top of JavaScript, allowing us the ability to work with JavaScript while expanding functionality to help keep our app organized and less error prone.

TypeScript is a strongly typed programming language which builds on JavaScript giving you better tooling at any scale. — typescriptlang

Why use TypeScript ?

At some point in your developer journey, you’ve probably found yourself debugging a component or function to determine the type of value it expects. When I’ve had to do this, I usually find myself logging values or using a debugger to analyze variables and determine the object structure. This is cumbersome and takes time.

TypeScript helps us avoid this because when working on a TypeScript project, we define the type of variables, props, and parameters expected by components and functions. Additionally, our code editor’s IntelliSense can suggest expected types, helping us identify expected values even faster.

Consider the error image above, where we’ve passed an incorrect parameter into a function. Just by reading the error, we can quickly infer that our parameter type must be a string and not a number. No only that, but we won’t even be able to compile our code if we don’t fix the problem because TypeScript will throw an error when incorrect values are passed. In a project without TypeScript, we would have realized this only after running the function. We’re avoiding the problem altogether. Pretty Neat!

Basic Types

Before we start declaring type annotations, we should note that JavaScript comes with seven built in types, the JavaScript primitives. These are:

We can output the type of variable we’re working with by evaluating console.log(typeof variableName). We will be using these primitives along with other types for our type annotations.

Any, Void, and Unknown Types

Any type
function errorLogger(complexRemoteObj: any) {
...
}
Void type
const logError(errors: string []) :void => {
for(let error of errors) {
console.log(error)
}
}
Unknown type
const emailError(error1: unknown, error2: unknown) {
if(typeof error1 === 'server' && typeof error2 === 'data') {
...sending email
}
}
Try it out here

Before diving into type declarations, let’s go over the any, void, and unknown types.

The any type is how we tell TypeScript not to run any type checks on the value we’re defining. It was originally used for when TypeScript was unable to accommodate complex objects, but its use is now mostly discouraged because TypeScript can accommodate most complex objects.

The void type is used to explicitly define that a function does not return anything. TypeScript can infer the return types so this is not really needed unless we want to emphasize that the function has no return value.

The unknown type is similar to the any type in terms of allowing any value but when we use the unknown type, Typescript checks are not disabled. We can check the types of values pass in order to perform operations. The unknown type helps keep our code type-safe.

Defining Types

In order to benefit from type checking, we must first do a bit of setup work by declaring types when we first code our features.

The most common ways of defining types are: type aliases, type interfaces, and inline type annotations. In most cases interfaces and type aliases are used as these help us reuse the types throughout the app, helping to keep our code dry, and organized.

Inline Type Annotations

Common Declarationsvariable
const hometown: string = 'NYC';
array
const numberArray: Array<number> = [1,2,3];
or
const stringArray: string[] = ['a','b','c'];
tuple
const pet: [string, number] = ["Toby", 27];
object
const pet: {name: string; age: number;} = {
name: 'Toby';
age: 27;
}
Try it out here

Inline type annotations is an easy and convenient way to declare types. For variables, the syntax is: variable name, a colon, followed by the variable type, and the value definition (“variableName: variableType = value”). In our variable definition above, we’ve declared a hometown variable of type string and set it to NYC. I’ve also included other common inline declarations that are useful to know when declaring types and interfaces.

Inline Function DeclarationsRegular function
function greetNthTimes(greeting: string, xTimes: number) : string {
let str = ''
for(let i = 0 ; i <= xTimes; i ++) {
str += `${greeting} \n`
}
return str
}
console.log(greetNthTimes('hi', 10))
Arrow function
const greetNthTimes = (greeting: string, xTimes: number) : string => {
let str = ''
for(let i = 0 ; i <= xTimes; i ++) {
str += `${greeting} \n`
}
return str
}
console.log(greetNthTimes('hi', 10))
Try it out here

We can assign type annotations to functions, including arrow functions. Parameters are followed by a colon and its type. In our first function above, we define a function that takes a greeting and it will print it on a new line x times. Their respective parameters are string and the number.

Sometimes we create functions with optional parameters. To make a parameter optional, we add a question mark(?) after our parameter name and and before our type declaration. Keep in mind that when working with inline type annotations, we can only make our last parameter optional. If we need more flexibility, we can declare interfaces or type aliases.

Type Aliases

type Person = {
age: number;
name: string;
email?: string;
}
Try it out here

A type alias is an alias for a declared type that we can then reference in different parts of our app and in other aliases or interfaces. To declare a type alias, we preface our declaration with the word type, followed by the capitalized desired alias name, and we set it equal to an object that contains our type annotations.

In the above example, we’ve declared a person type alias with an age property of type number and a name property of type string. We’ve also declared an optional email parameter because not every person has an email.

Type Interfaces

Interface Person {
age: number;
name: string;
email?: string;
}
Try it out here

An interface is very similar to type aliases. We declare an interface by prefacing our desired interface name with “Interface” and opening an object where we declare our type annotations. As you can see from the example above, type aliases look nearly identical to interfaces; In most cases, they’re interchangeable. The TypeScript docs note their differences as “the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.”

Union Types

type FavoriteChar: string | number | Symbol | null

Union types are types formed by combining multiple existing types. We declare a union type by separating existing types with a pipe character (|). Our favoriteChar type above can accept any of the types we’ve included in our union type. If we need to be more explicit about the values we want to allow, we can use a string literal type.

type Borough: "Queens" | "Brooklyn" | "Staten Island" | "Bronx" | "Manhattan" 

String literal types allow us to narrow down the allowed types to the specified values. If we had a function that expected a NYC borough, we could use the string literal union type above to ensure we only accept the 5 boroughs.

Intersection Types

type Name {
name: string;
}
type Age {
age: number
}
type Breed {
breed: string
}
type Pet = Name & Age & Breed
const toby: Pet {
name: 'toby';
age: 27;
breed: 'lab';
}
Try it out here

Intersection types are similar to union types in that they allow us to form a new type using existing types but an intersection will be formed of all the types used to create the intersection. We define an intersection type by using the ampersand(&) sign between the types we’ll be using. In our example above, we’ve used three different types to form our Pet type.

Generic Types

Array Generic Type 
const stringArray: Array<string> = ['hi','hello','i']
const numberArray: Array<number> = [1,2,3]

Generic types allow us to define types at definition by passing the types as comma-separated-values inside angle brackets. One of the generic types you may encounter most often is the array generic type as seen above.

Regular Generic function 
function logChar<ItemType1>(char: ItemType1, x: number) :void {
for(let i = 0 ; i <= x; i++) {
console.log(char)
}
}
console.log(logChar<string>('howdy!', 10))
Arrow Generic function
const logNum = <ItemType1,>(char: ItemType1, x: number) => {
for (let i = 0; i <= x; i++) {
console.log(char);
};
}
Try it out here

Similarly, we can declare generic functions by passing generic parameters in angle brackets and then referencing them in our function parameters. We can even declare generic arrow functions with one caveat: we must use a comma after our generic parameter declaration because the compiler will otherwise think we’re working with a react element.

Generic type aliases and interfaces

interface Form<Type1> {
value: Type1;
}
type Form<Type1> {
value: Type1;
}

Declaring generic type aliases and interfaces is similar. We declare the interface or type name followed by the angle brackets with our placeholder parameters and then reference them within our type or interface.

TypeScript may add a bit of extra code and effort to the development process but the benefits we get through type checking and IntelliSense are well worth it. I hope you get to try it out on your next project! This concludes this first post on TypeScript’s basics. Stay tuned for the next post on using TypeScript with React.

Full Stack Developer — Runner — NYC