DEV Community

Cover image for TypeScript
Kevin Odongo
Kevin Odongo

Posted on

TypeScript

Hey Dev's

It's been a while catching up with everyone here. Let me tell you a short story of how today's tutorial is coming up from. Back in March, we got a client who needed a project, and in his condition, we had to use React, TypeScript, and Tailwind. Guess who had a basic understanding of React and TypeScript. It's funny we accepted the challenge so I had to go through sleepless nights of learning in-depth about React and TypeScript.

Alt Text

I was truly amazed by TypeScript!!
Truth be told knowing JavaScript made it easier. All hearsay I have heard about TypeScript came to reality. It truly makes an application robust and catches bugs early. However much you are limited to the packages you can use in your application it is still worthwhile.

While learning through I created a cheat sheet that kept me going through the project. We completed the project and the client is happy with the results. Let me share what I learnt it might help someone who is planning on learning TypeScript.

I wonder why Flow Language has not penetrated the market as TypeScript guess the latter is backed by a big corporation and has a supportive community. Anyway, that's just my opinion.

Take a look at this:

const User = {
   name: "Kevin Odongo"
}
User.name = 1
// In JavaScript this will compile.
// In TypeScript this will fail. (Type 'number' is not assignable to type 'string')
// User.name is a string but we are trying to assign a number to it

Enter fullscreen mode Exit fullscreen mode

This is quite helpful during the development stage because you will catch the errors in advance before your application goes to production. Truth be told this is what made me love it.

How to get started with TypeScript.

// Install TypeScript globally
npm install -g typescript

// React
npx create-react-app my-app --template typescript

// Vue
vue create my-app
- Manually select and choose TypeScript
Enter fullscreen mode Exit fullscreen mode

Basics

Let me start by sharing the available types. Oh! what are Types? Good question. If you have a basic understanding of JavaScript you will know there are 7 primitive data types i.e string, number, bigint, boolean, undefined, symbol, and null. These data types are what TypeScript refers to when you hear the word Types.

Let us see how TypeScript works:

const User = {
   name: string // This indicate the name is a string
   age: number // This indicate the age is a number
   isAdmin: boolean // This indicate isAdmin is a boolean
}
// no error
User.name = "Kevin Odongo"
User.age = 10
User.isAdmin = true
// error
User.name = 1 // we are assigning a number to a string
User.age = "10" // we are assigning a string to number
User.isAdming = "admin" // we are assigning a string to boolean

// This will allow you to handle these errors during development.
// Assume you have a results of a function that should return a string and you want to use results.toLowerCase() OR results.toUpperCase() on the results if your results is null or undefined TypeScript will allow you to catch this error in advance.
Enter fullscreen mode Exit fullscreen mode

What about an array how do we declare an array?
Take a look at this

const array (any)[] = [] // An array that accepts anything
const array: (number)[] = [] // An array that accepts only numbers
const array: (string)[] = [] // An array that accepts only strings
Enter fullscreen mode Exit fullscreen mode

One more thing in case one of the variables we are declaring is not mandatory we just need to add a question mark while declaring the variable.

// Age and isAdmin are not mandatory in this object
const User = {
   name: string
   age?: number // not mandatory
   isAdmin?: boolean // not mandatory
}
Enter fullscreen mode Exit fullscreen mode

Other types that you should know are void, object, unknown, never and function.

Modifiers

We can modify our parameters or values as follows. We can change the visibility of the property.

  • readonly
  • private
  • protected
  • public
// name is readonly and cannot be mutated
const User = {
   readonly name: string
   protected age: number
   private isAdmin: number
   // By default all values are public
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our data types how do we use them within a function?
You can declare the type of parameters/values the function accepts.

Example

// here is a sample we have declared the parameters this function accepts and what are the data types for that function.
// This function updates user information and has declared the properties required to update the user and the types of those parameters

function updateUser(user: { name: string, age: number, isAdmin: boolean}){
   //...do something
}

// this can be written as follows too
type Props {
   name: string
   age: number
   isAdmin: boolean
}

function updateUser(user: Props){

}

instead of type Props {...}, we can use interface for example

interface Props {
   name: string
   age: number
   isAdmin: boolean
}

Enter fullscreen mode Exit fullscreen mode

So if I can declare a type and interface what is the difference between the two. The difference is that you can not extend a type but you can extend an interface.

Example

// user interface
interface User {
   name: string
   age: string
   isAdmin: boolean
}

interface Finance {
   department: string
}

// assuming we want the finance interface to access all the properties of User we can do it as follows:

interface Finance extends User {
   department: string
}
Enter fullscreen mode Exit fullscreen mode

Handling null and undefined in TypeScript can be approached in two different ways. With strictNullChecks on or off. With it on we need to test for both values while off we do not and can be accessed normally.

Here is something you should know we can declare a variable with ! to explicitly declare the variable is not null or undefined.

let bankBalance: number | null = 1000
console.log(bankBalance!.toFixed(2)) // this is explicitly declares the bankBalance is not null or undefined.
Enter fullscreen mode Exit fullscreen mode

Enum allows us to describe a value that can have a different set of constants.

Example

// assume we have a jacket in our e-commerce application which can be blue, red, or yellow
enum Jacket {
   blue,
   red,
   yellow
}

// We can thereby access different colors of Jacket
console.log(Jacket.blue)
console.log(Jacket.red)
console.log(Jacket.yellow)
Enter fullscreen mode Exit fullscreen mode

Let us look at an example of a function that returns something. How do we go about with this?

Example

// We can declare the return of this function to be a number.
function calculateSum(): number {
   return 45
}
Enter fullscreen mode Exit fullscreen mode

Unions are just a simple way of extending our variables to access different data types. Unions can be very useful and allow us to render more logic with different types allowed by the variable. We might want to use the variable differently if it is a string or number, just use typeof in your logic to get the current value provided.
The usage of typeof in getting information about our variables can be very powerful in narrowing our functions.

Example

// union
const User = {
   id: string | number // this is a union
}

// how to declare a union inline
function updateUser(name: string | null ){
   if(typeof name === 'string'){
     // ... do something
   } else {
     // ... do something
   }
}
Enter fullscreen mode Exit fullscreen mode

On my end, this is the basic understanding of TypeScript I had when I got into the client project. It was scary at first but with technology given opportunity, there is no language difficulty to learn. Let us dig deeper and get to know more of what i learnt.

Alt Text

Narrowing in TypeScript is more similar in JavaScript the only difference is that in TypeScript we have declared our variable data types which makes it more precise.
In JavaScript, we can use any expression in conditionals, &&s, ||s, if statements, and Boolean negations (!), equality checks like ===, !==, ==, and != to narrow types, in and instaceof and more. We can use all of these in TypeScript.

Example

// here we have an interface that has a name with unions of string, null and undefined.

interface CheckName {
    name: string | null | undefined
}

// in this function our condition has already eliminated null and undefined. This gives us a safety net.
function updateUser (user: CheckName){
   if(user.name != null){
    // ... do something
  }
}

Enter fullscreen mode Exit fullscreen mode

When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.

Example

// This function checks the roles available in the application
interface Admin {
   kind: 'admin'
   // can be extended
}

interface Regular {
   kind: 'regular'
   // can be extended
}

type Roles = Admin | Regular;

// This function checks the user role and redirects them to user dashboard or admin dashboard.
function getRoles(role: Roles) {
  switch (role.kind) {
    case "admin":
      return // ... go to admin dashboard
    case "regular":
      return // go to regular dashboard
    default:
      const _exhaustiveCheck: never = role;
      return _exhaustiveCheck;
  }
}
Enter fullscreen mode Exit fullscreen mode

Functions are the backbone of our applications. In our basics we discussed the basic knowledge of how to deal with functions. Here we will expound that scope of what we learnt.

We can render a function in an interface or type as follows:

// type
type Props {
   fn: () => void
}

// interface
interface Props {
   fn: (name: string) => void
}
Enter fullscreen mode Exit fullscreen mode

In TypeScript, generics are used when we want to describe a correspondence between two values

Example

// This function has created a link between the input (passed array) and output (returned array)
function getAdmin<Type>(users: Type[]): Type {
   return arr.filter(user => user.role === isAdmin)
}

// the function gets an array of users and returns users who are only admins.
Enter fullscreen mode Exit fullscreen mode

To go further you can limit the parameter type by declaring a props and passing in the types.

Function overloads allows us to write functions that accepts a variety of arguments.
In TypeScript, we can specify a function that can be called in different ways by writing overload signatures. To do this, write some number of function signatures (usually two or more), followed by the body of the function. The implementation signature must also be compatible with the overload signatures. For example, these functions have errors because the implementation signature doesn’t match the overloads in a correct way:

Example

// we can get the length of a string and the length of an array
// our implementation will take an array or string and return the length of the value provided
// the signatures about will handle the computation provided in the implementation function
// Note that both our signatures return Type is a number.

function length(s: string): number; // signature function
function length(arr: any[]): number; // signature function
// implementation
function length(x: any[] | string) { 
  return x.length;
}
Enter fullscreen mode Exit fullscreen mode

interfaces allowed us to build up new types from other types by extending them. TypeScript provides another construct called intersection types that is mainly used to combine existing object types.

Example

interface User {
   name: string
   age: age
   isAdmin: boolean
} 

interface Finance {
   department: string
}

// we have combined Finance and User interfaces.
type FinanceUser = User & Finance
Enter fullscreen mode Exit fullscreen mode

The two areas i found very interesting are modules, namespaces and classes. They truly changed me as a developer. To those who started with Angular as a Framework learned TypeScript the easy way. With Vue and React you have to learn it separately then incorporate it in your application.

NOTE

Take more emphasis in understanding classes, modules and namespaces

Take a look at this example

// declare a user class
export class User {
    name: string
    age: number
    isAdmin: boolean
}

// import it in another file and access all the properties.
import { User } from "./User"

const newUser = new User()
newUser.name = "Kevin Odongo"
newUser.age = 10
newUser.isAdmin = true
Enter fullscreen mode Exit fullscreen mode

That is just a basic example now let us expound further.
Example

// declare a user class
export class User {
    name: string
    age: number
    isAdmin: boolean

   constructor(){
      // we have access to all the properties using this
      this.name = "Kevin Odongo"
      this.age = 10
      this.isAdmin = true
   }
}

// we can simplify this further whichever way you want you use either is possible.
export class User {
   constructor(name: string, age: number, isAdmin: boolean){
      // we have access to all the properties using this
      this.name = "Kevin Odongo"
      this.age = 10
      this.isAdmin = true
   }
}

Enter fullscreen mode Exit fullscreen mode

We can therefore declare our classes in one file and export all of them. This is quite impressive because it allows us to separate our application with components that can be reused anywhere.

Here is another example

// user class
class User {
  name: string
  age: number
  isAdmin: boolean
}

// extend user to Finance
class Finance extends User {
  constructor() {
    super();
    //... we can access all the User properties
    this.name = "Kevin Odongo"
  }
}
Enter fullscreen mode Exit fullscreen mode

We can create a class with complete getters and setters.

// this is a class the has a getter and setter
exports class User {
  _value: {
     _name: string
     _age: number
     _isAdmin: boolean
  }

  get user() {
    return this.value;
  }
  set user(value) {
    this._value = value;
  }
}
Enter fullscreen mode Exit fullscreen mode

You can use an implements clause to check that a class satisfies a particular interface. An error will be issued if a class fails to correctly implement it:

Example

// this is a class the has a getter and setter
interface Props {
   _name: string
   _age: number
   _isAdmin: boolean
}
// the props is just a checker
exports class User implements Props {
   _value: Props

  get user() {
    return this.value;
  }
  set user(value) {
    this._value = value;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can override the base class functions by adding super(). An abstract method or abstract field is one that hasn’t had an implementation provided. These members must exist inside an abstract class, which cannot be directly instantiated.

If you have a file that doesn’t currently have any imports or exports, but you want it to be treated as a module, add the line:

export {}
Enter fullscreen mode Exit fullscreen mode

The main thing to consider when writing a module based application are:

  • Syntax
  • Module Resolution
  • Module OutputTarget

Here is an example of a module
Example

// user.ts.
export function users (){
   return "Kevin Odongo"
}

// main.ts
import { User } from "./User"
// access
User()

Enter fullscreen mode Exit fullscreen mode

*TypeScript has its own module format called namespaces which pre-dates the ES Modules standard. This syntax has a lot of useful features for creating complex definition files, and still sees active use in DefinitelyTyped. *

TypeScript provides several utility types to facilitate common type transformations. These utilities are available globally.
Here are the list of the utilities

  • Partial
  • Required
  • Readonly
  • Record
  • Pick
  • Omit
  • Exclude
  • Extract
  • NonNullable
  • Parameters
  • ConstructorParameters
  • ReturnType
  • InstanceType
  • ThisParameterType
  • OmitThisParameterType
  • ThisType
  • InstristicStringManipulation
  • Uppercase /LowerCase / Capitalize / Uncapitalize

These are many utilities, You do not have to know all of them but you can always reference depending on what you need to implement. Just have a basic understanding on how the work and how to implement them.

Here are a few example to give you a better understaning of working with utilities.

interface Props {
   name: string
   age: number
   isAdmin: boolean
}

// the partial extends all the subset in Props.
function updateUser(user: Props, fields: Partial<Props>){
    return { ...user, ...fields}
}

// required utility
// this will throw an error because all the properties in props are required.
const user: Required<Props> = { name: "Kevin Odongo"}

// readonly
// this will throw and error because the Props are only readonly
const user: Readonly<Props> = { name: "Kevin Odongo"}


// pick
// you can add union to access more properties
const adminUsers: Pick<Props: 'isAdmin'>
const admin: adminUsers = {
    isAdmin: true
}
Enter fullscreen mode Exit fullscreen mode

Decorators

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

Types

  • Methods Decorator
  • Class Decorator
  • Accessor Decorator
  • Property Decorator
  • Parameter Decorator

In Angular framework decorators are used mostly to separate modification or decoration of a class without modifying the original source code.

Mixin

TypeScript’s best mixin support is done via the class expression pattern.

Example

// Each mixin is a traditional ES class
class Jumpable {
  jump() {}
}

class Duckable {
  duck() {}
}

// Including the base
class Sprite {
  x = 0;
  y = 0;
}

// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);

let player = new Sprite();
player.jump();
console.log(player.x, player.y);

// This can live anywhere in your codebase:
function applyMixins(derivedCtor: any, constructors: any[]) {
  constructors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
          Object.create(null)
      );
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Modules and Namespaces

We have gone through how to import and export modules. Let me go through some examples.

// OPTION 1
// file __User.ts__
export class User {...}
export class Finance {...}

// import on other components
import { User, Finance} from "./User"

// OPTION 2
class User {...}
class Finance {...}

module.exports ={
   User,
   Finance
}

// import on other components
import { User, Finance} from "./User"

// declare a module
// this will be ideal with npm packages that do not have declaration files.
// file __d.ts__
declare module "Name"
Enter fullscreen mode Exit fullscreen mode

Do not use namespaces in modules

Namespaces are simply named JavaScript objects in the global namespace. This makes namespaces a very simple construct to use. Unlike modules, they can span multiple files, and can be concatenated using --outFile. Namespaces can be a good way to structure your code in a Web Application, with all dependencies included as script tags in your HTML page.

Example

// __validation.ts__
namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }
}

/// <reference path="Validation.ts" />
// __LettersOnlyValidator.ts__
namespace Validation {
  const lettersRegexp = /^[A-Za-z]+$/;
  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above example you will not how to declare a namespace and import in a file. Alternatively we can import them or use them as script tags in index.html files.

// using tags in index.html
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />

// using import
import {} from "".
Enter fullscreen mode Exit fullscreen mode

Alt Text

You made it to the end. I hope you enjoyed the tutorial. Ops! one more thing

Declaration files

Here is an example from TypeScript docs. For this go through in detail the TypeScript documentation in case you want to publish your application to npm

// your code __code.ts__
let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);
let count = myLib.numberOfGreetings;

// declaration
declare namespace myLib {
  function makeGreeting(s: string): string;
  let numberOfGreetings: number;
}
Enter fullscreen mode Exit fullscreen mode

CONCLUSION
Going through this article should get you from beginner to a more comfortable position in working with TypeScript. It is a language that i have come to appreciate and enjoy. Will refactor some of my personal projects and stick to it henceforth.

See you in the next tutorial

Top comments (0)