TypeScript Basics

Getting started with TypeScript


After declaring the variable and before assigning it, use : to specify the types

let name: string // Can takes string value
let age: number // Takes number
let isStudent: boolean // Takes boolean
let hobbies: string[] // takes array of string (read from right to left)
 
let role: [number, string] // this defines a tuple that will take a number and then a string
role = [5, 'typescript']

Defining Objects

let person: Object // Just checks if person is of type object or not
 
// To defined entire structure use type or interface
 
type Person = {
  name: string
  age: number
}
 
let person: Person = {
  name: 'test',
  age: 25,
}
 
// Now age is optional
type Person = {
  name: string
  age?: number
}
 
let person: Person = {
  name: 'test',
}
 
let selectedPerson: Person[] // Array of type Person
 
// We can use interface also instead of type
interface Point {
  x: number
  y: number
}
 
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x)
  console.log("The coordinate's y value is " + pt.y)
}
 
printCoord({ x: 100, y: 100 })

Union Type

let salary: number | string // Can take either number or string

Defining functions

function printName(name: string) {
  console.log(name)
}
 
printName('Adesh')
 
// Function with return values
function squareNumber(a: number, b: number): number {
  return a * b
}
 
// Also we can define void
function printName(name: string): void {
  console.log(name)
}
 
printName('Adesh')

Defining arrow functions

const sum = (x: number, y: number): number => {
  return x + y
}

The above methods work when you are defining the function, when you need to just declare the function in a interface or type, use the following syntax :

interface Props {
  todo: string
  handleAdd: () => void // Declares a function that will not return anything
  sum: (x: number, y: number) => number // Declare a function which takes 2 numbers and returns a number
}

Defining object parameters

function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x)
  console.log("The coordinate's y value is " + pt.y)
}
printCoord({ x: 4, y: 5 })

Types Vs Interfaces

Extending Types

type Animal = {
  name: string
}
 
type Bear = Animal & {
  honey: boolean
}
 
const bear: Bear = {
  name: 'Hary',
  honey: true,
}

Extending Interfaces

interface Animal {
  name: string
}
 
interface Bear extends Animal {
  honey: boolean
}
 
const bear: Bear = {
  name: 'Hury',
  honey: false,
}

Adding new fields to an existing interface

interface Person {
  name: string
}
 
interface Person {
  age: number
}
 
const jarrod: Person = {
  name: 'Jarrod',
  age: 32,
}

Adding new fields / properties does not work in type

type Person = {
  name: string
}
 
type Person = {
  age: number
}
 
// Error: Duplicate identifier 'Person'.

Summary:

Interfaces and types can mostly be used interchangeably.

  1. Interface can be implemented by classes (supports class method)
  2. Interfaces can extend each other
  3. Types can do unions and intersections
  4. Interfaces are used for objects where as types can be used for anything.

Defining types for Actions in Reducer hook

type Actions =
        | {type: 'add', payload: string}
			  | {type : 'remove', payload : number} // since payload is id
			  | {type : 'done', payload : number} // again number
 
 
const TodoReducer = (state : Todo[], action : Actions) => {
    // Usual stuff
    switch(action.type){
        case 'add' :
            return [
                ...state,
                {id : Date.now(), todo : action.payload, isDone: false}
            ]
        case 'remove' :
            return state.filter((todo) => todo.id != action.payload)
        case 'done' :
            return state.map((todo) => todo.id !== action.payload ? {...todo, isDone : !todo.isDone})
        default:
             return state
    }
}
 
 
const [state, dispatch] = useReducer(TodoReducer, [])

Type Assertions Vs Type annotations

The as keyword is used for type assertions. It tells TypeScript that you're certain about the type of a value, even if TypeScript's type inference might suggest a different type.

const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement

The : syntax is used for type annotations. Type annotations explicitly specify the type of a variable or a function parameter. They're helpful when you want to provide type information that TypeScript cannot infer automatically.

let myName: string = 'Alice'

In most cases, TypeScript's type inference is sufficient, and you don't need to explicitly provide type annotations.

Generics

Consider this example where you have the same function for two different data types:

// for number type
function fun(args: number): number {
  return args
}
// for string type
function fun(args: string): string {
  return args
}

You can use generics to write a single function that works for both data types:

function fun<T>(args: T): T {
  return args
}
 
fun<number>(42) // 42
fun<string>('hello') // 'hello'

In this example, <T> is a type parameter that represents the type that the function will work with. When calling the function, you can specify the type you want to use, making the code more flexible and type-safe.

Generics with Classes
class Stack<T> {
  private items: T[] = []
 
  push(item: T) {
    this.items.push(item)
  }
 
  pop(): T | undefined {
    return this.items.pop()
  }
}
 
const numberStack = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
console.log(numberStack.pop()) // 2
 
const stringStack = new Stack<string>()
stringStack.push('a')
stringStack.push('b')
console.log(stringStack.pop()) // 'b'

In this example, the Stack class has a type parameter <T>, which allows it to work with different data types while maintaining type safety.

Generics with Interfaces
interface KeyValuePair<T, U> {
  key: T
  value: U
}
 
const kv1: KeyValuePair<string, number> = { key: 'one', value: 1 }
const kv2: KeyValuePair<number, string> = { key: 2, value: 'two' }

Here, the KeyValuePair interface has two type parameters, <T> and <U>, representing the types of the key and value properties, respectively

Partials

Used to get some elements from a type. Essentially make all the types optional so that we have the flexibility to choose what we want to send.

interface Todo {
  id: number
  title: string
  description: string
  done: boolean
}
 
type UpdateTodo = Partial<Todo>

Usage

function updateTodo(id: number, newProps: UpdateTodo) {
  // patch request to update the TODO in the DB
}
 
updateTodo(1, {
  description: 'Go to Gym',
})
 
// or
 
updateTodo(2, {
  done: true,
})

Pick

Similar to Partial we have Pick which allows you to construct a new type by selecting properties (Keys) from an existing type (Type).

Pick<Type, Keys>

Example:

interface Todo {
  title: string
  description: string
  completed: boolean
}
 
type TodoPreview = Pick<Todo, 'title' | 'completed'>
 
const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
}