Skip to main content

Typescript 2023

TST, Hongkong

Typescript Setup

npm init -y
npm install --save-dev typescript
mkdir -p ./src && touch ./src/index.ts
echo 'console.log("Hello World!")' > ./src/index.ts

TypeScript Compiler

tsc --init

``./tsconfig.json`

{
  "include": ["./src"],
  "exclude": ["./node_modules", "./src/bak"],
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */
    /* Language and Environment */
    "target": "es2022",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "lib": ["DOM", "ES2022"],                            /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    /* Modules */
    "module": "ES2022",                                /* Specify what module code is generated. */
    "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    "outDir": "./dist",                                   /* Specify an output folder for all emitted files. */
    "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}

Add the following npm scripts to your ./package.json file:

"scripts": {
    "tsc": "tsc --watch",
    "start": "node ./dist/index.js"
  }

And execute both scripts in two separate terminals:

npm run tsc
npm run start

> typescript-2023@1.0.0 start
> node ./src/index.js

Hello World!

Playground

Functions

const greeting = (name: string = 'World'): string => {
    return `Hello ${name}!`
}

console.log(greeting())
function square(number: number): number {
    return (number * number)
}

const currency = (string: string = '$'): string => {
    return (string)
}

console.log(square(16), currency('HK$'))
// Every year that is exactly divisible by four is a leap year, except for years
// that are exactly divisible by 100, but these centurial years are leap years if
// they are exactly divisible by 400.

function leapYear(year: number): boolean {
    let isLeapYear: boolean

    if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
        return isLeapYear = true
    } else {
        return isLeapYear = false
    }
}

console.log(leapYear(2012))
console.log(leapYear(2013))662607

const array: (string|number)[] = ['sdfgdgsdf', 'asdfdsgdfg', 'agdfgdsfg', 'gds']

const assignment = (array: (string|number)[]): string|number  => {
    let string!: string;

    for (let seq of array) {
        if (typeof seq !== "number") {
            if(seq.length === 3){
                string: string = seq
            }
        } else {
            return seq * seq
        }
    }

    return string
}

const consolePrint = (array: (string|number)[]): void => {
    console.log(assignment(array))
}

consolePrint(array)

Objects

let product1: {title: string, type: string[], price: number, readonly availability?: boolean} = {
    title: 'IN-8415 2K+ WQHD',
    type: ['indoor', 'wqhd', 'pt'],
    price: 149.99
}

let product2: {title: string, type: string[], price: number, readonly availability?: boolean} = {
    title: 'IN-9420 2K+ WQHD',
    type: ['outdoor', 'wqhd', 'ptz'],
    price: 299.99,
    availability: true
}

function printProduct(
    product: {
        title: string,
        type?: string[],
        requirements?: Requirements,
        price: number,
        readonly availability?: boolean,
        shop?: string
    }): void {
        if (!product.availability) {
            console.log(`ERROR :: ${product.title} not available!`)
        } else if (!product.requirements) {
            console.log(
                `Camera\n Camera Model: ${product.title}\n Camera Type: ${product.type}\n Camera Price: ${product.price}`
            )
        } else if (!product.shop) {
            console.log(
                `Software\n Title: ${product.title}\n Requirements: \n    Operating System: ${product.requirements.os}\n    Minimum Version: ${product.requirements.minVersion}\n    Minimum Storage (MB): ${product.requirements.storageMb}\n Software Price: ${product.price}`
            )
        } else {
            console.log(
                `Software\n Title: ${product.title}\n Requirements: \n    Operating System: ${product.requirements.os}\n    Minimum Version: ${product.requirements.minVersion}\n    Minimum Storage (MB): ${product.requirements.storageMb}\n Software Price: ${product.price}\n Download: ${product.shop}`
            )
        }
}

printProduct(product1)

printProduct(product2)
type Requirements = {
    os: 'Windows' | 'macOS' | 'LINUX' | 'Android' | 'iOS';
    minVersion: number;
    storageMb: number;
}

type SoftwareProduct = {
    title: string;
    requirements: Requirements;
    price: number;
    readonly availability?: boolean;
}

let product3: SoftwareProduct = {
    title: 'InstarVision 3',
    requirements: {
        os: 'Windows',
        minVersion: 8.1,
        storageMb: 74 
    },
    price: 24.99,
    availability: true
}

printProduct(product3)
type MobileSoftware = {
    shop: string;
}

let product4: SoftwareProduct & MobileSoftware = {
    title: 'InstarVision App',
    requirements: {
        os: 'Android',
        minVersion: 14,
        storageMb: 23
    },
    price: 2.99,
    availability: true,
    shop: 'Google Play'
}

printProduct(product4)

Arrays

const indoorCameras: string[] = []

indoorCameras.push('in-8415', 'in-8403', 'in-8401')

console.log(indoorCameras)
type Point = {
    x: number;
    y: number;
    z: number
}

const coords: Point[] = []

coords.push(
    {x: 212, y: 54, z: 23},
    {x: 324, y: 56, z: 63},
    {x: 223, y: 76, z: 34}
)

console.log(coords)
const tictac: (string | number)[][] = [
    ['X', '0', 'X'],
    ['0', 'X', 'O'],
    ['X', '0', 'X']
]

console.log(tictac)
type Product = {
    name: string;
    price: number
}

const basket: Product[] = [
    { name: 'in-8415', price: 149.99 },
    { name: 'in-8403', price: 129.99 },
    { name: 'in-8401', price: 109.99 },
]

const getTotal = (basket: Product[]): number => {

    let priceArray: number[] = []

    for (let order of basket) {
        priceArray.push(order.price)
    }

    const total = priceArray.reduce((a, b) => a + b)
    
    return total
}

console.log(getTotal(basket))

Tuples & Enums

Tuples only exists in Typescript and are arrays of a fixed length and are ordered with specific types:

let tuple: [string, number]

// tuple = [149.99, 'in-8415'] not assignable
tuple = ['in-8415', 149.99]
type ServerError = [
    500 | 501 | 502 | 503 | 504,
    'Internal Server Error' | 'Not Implemented' | 'Bad Gateway' | 'Service Unavailable' | 'Gateway Timeout'
]
    
const serverErrorResponse: ServerError = [503, 'Bad Gateway']

Enums are a set of named constants:

const enum ServerResonse {
    OK = 200,
    UNAUTHORIZED = 401
}

const makeRequest = (): void => {
    let status = 200
    if (status === ServerResonse.OK) {
        console.log('INFO :: API request was successful')
    }
}

makeRequest()

Interfaces

interface AlarmEvent {
    type: string
    trigger: string[]
    object?: string[]
    readonly timestamp: string
}

interface AlarmPush extends AlarmEvent {
    push: (timestamp: string, type: string) => string
}

const alarm: AlarmPush = {
    type : 'Motion Detection',
    trigger : ['Area1', 'Area2', 'PIR'],
    object : ['Car','Person'],
    timestamp : 'Monday, January 15, 2024 AM01:20:39 HKT',
    push: (timestamp, type) => {
        return `${timestamp} :: ${type}`
    }
}

const pushNotification = (event: AlarmPush): void => {
    console.log(event.push(event.timestamp, event.type))
}

pushNotification(alarm)

Classes

interface OperatorUser {
    username: string,
    readonly email: string,
    privileges: string[]
}

class Operator implements OperatorUser {

    constructor(
        public username: string,
        protected readonly _email: string,
        protected _privileges: string[] = ['operator']
    ) {}

    private notify(email: string): void {
        console.log(`INFO :: Email send to ${email}`)
    }

    alarm(): void {
        this.notify(this.email)
    }

    get email(): string {
        return this._email
    }

    get privileges(): string[] {
        return this._privileges
    }

    get userPrivileges(): string {
        return `INFO :: Active user privileges ${this._privileges}`
    }

    get nickname(): string {
        return `INFO :: Active user nickname ${this.username}`
    }

    set nickname(newUsername: string) {
        if (newUsername.length > 3) {
            this.username = newUsername
        } else {
            console.log('ERROR :: Username must be longer than 3 characters!')
        }
    }

}

Class inheritance:

interface AdminUser extends OperatorUser {
    isAdmin: boolean
}

class Administrator extends Operator implements AdminUser {
    
    constructor(
        username: string,
        readonly _email: string,
        _privileges: string[] = ['administrator'],
        public isAdmin: boolean = true
        ) {
            super(username, _email, _privileges)
        }
    
    addPermissions(permission: string) {
        this.privileges.push(permission)
    }

}

const administrator = new Administrator('admin', 'admin@instar.com', ['administrator'])

console.log('INFO :: Active user is admin: ', administrator.isAdmin)

administrator.alarm()


console.log(administrator.nickname)
administrator.nickname = 'Mike'
console.log(administrator.nickname)

console.log(administrator.userPrivileges)
administrator.addPermissions('superadmin')
console.log(administrator.userPrivileges)

Generics

Provide a type when your generic function is being executed:

function id<genericType>(item: genericType): genericType {
    return item
}

console.log(id<string>('1337'))
console.log(id<number>(1337))
interface Parts {
    id: number
    article: string
}

const getRandomElement = <T = number>(list: T[]): T => {
    const randIndex = Math.floor(Math.random() * list.length)
    return list[randIndex]
}

console.log(getRandomElement<string>(['a','b','c','d','e']))
console.log(getRandomElement<number>([0,1,2,3,4]))
console.log(
    getRandomElement<Parts>([
        { id: 1, article: 'EF5436' },
        { id: 2, article: 'EF3245' },
        { id: 3, article: 'EF1245' },
        { id: 4, article: 'EF6794' },
        { id: 5, article: 'EF2358' },
    ])
)
interface RMA {
    rmaid: number
    defect: string
}

function merge<T,U>(prod: T, part: U) {
    return {
        ...prod,
        ...part
    }
}

let productToRepair = merge<RMA, Parts>(
    { 'rmaid': 456, 'defect': 'no wifi connection' },
    { 'id': 1, article: 'EF5436'} 
)

console.log(productToRepair)

Type Narrowing

Using a typeof Type Guard to narrow down union types:

function doMath(value: number | string): string {
    if (typeof value === 'number') {
        return 'INFO :: The triple value is: ' + value * 3
    } else {
        return 'INFO :: The triple value is: ' + Number(value) * 3
    }
}

console.log(doMath(33))
console.log(doMath('33'))
console.log(doMath('EE'))

Using a Truth Guard to narrow down your types:

function registerUser(nickname: string | null, email: string| null): void {
    if (!nickname) {
        console.log('ERROR :: You need to add a nickname for new user!')
    } else if (!email) {
        console.log('ERROR :: You need to add an email for new user!')
    } else {
        console.log(`INFO :: User ${nickname} registered sucessfully!`)
    }
}

registerUser(null, null)
registerUser('player1', null)
registerUser('player1', 'player1@game.com')

Equality narrowing:

function compareFunc (x: number | string, y: number) {
    if (x === y) {
        console.log('INFO :: Both variables are of type numbers!')
    } else  {
        console.log('INFO :: Variable x is of type string!')
    }

}

compareFunc(12, 12)
compareFunc('12', 12)

IN Operator

interface IEEE1394 {
    AS5643: boolean
}

interface USB {
    TypeC: boolean
}

const connector = (iface: IEEE1394 | USB ): void => {
    if ('AS5643' in iface) {
        console.log(`INFO :: AS5643 interfcae available: ${iface.AS5643}`)
    } else {
        console.log(`INFO :: TypeC interfcae available: ${iface.TypeC}`)
    }
}

const iface: IEEE1394 = {AS5643: true}

connector(iface)

instanceof

function prettyUTCDate(date: string | Date) {
    if (date instanceof Date) {
        console.log('INFO :: ', date.toUTCString())
    } else {
        let now = new Date(date)
        return 'INFO :: ' + now.toUTCString() + '(from string)'
    }
}

let date = new Date()

prettyUTCDate(date)
console.log(prettyUTCDate(String(date)))

Type Predicates

interface HardProduct {
    name: string
    type: string
}

interface SoftProduct {
    name: string
    os: string
}

let hardwareList: string[] = ['Hardware Products']
let softwareList: string[] = ['Software Products']

function isSoftware(product: HardProduct | SoftProduct): product is SoftProduct {
    return (product as SoftProduct).os !== undefined
}

function listProducts (product: HardProduct | SoftProduct): void {
    if (isSoftware(product)) {
        softwareList.push(product.name)
        console.log(softwareList)
    } else {
        hardwareList.push(product.name)
        console.log(hardwareList)
    }
}

let product_01: HardProduct = {
    name: 'IN-8415',
    type: 'Indoor Camera'
}

listProducts(product_01)

let product_02: SoftProduct = {
    name: 'InstarVision',
    os: 'Windows'
}

listProducts(product_02)

Discriminated Unions

interface HardKind {
    name: string
    __type: 'hardware'
}

interface SoftKind {
    name: string
    __type: 'software'
}

type AllProducts = SoftKind | HardKind

function productDiscriminator(product: AllProducts) {
    switch(product.__type) {
        case('hardware'):
            return 'INFO :: Hardware Product'
        case('software'):
            return 'INFO :: Software Product'
        default:
            // If we end up here something went wrong
            const __exhaustiveCheck: never = product
    }
}

let mysteryProduct1: HardKind = {
    name: 'ICB Halford 2000',
    __type: 'hardware'
}

let mysteryProduct2: SoftKind = {
    name: 'Davinci Resolve Studio',
    __type: 'software'
}

console.log(productDiscriminator(mysteryProduct1))

console.log(productDiscriminator(mysteryProduct2))

Organizing Code in ES Modules

To use import/export syntax in your codebase make sure that you configure TS to generate a browser-conform output:

./tsconfig.json

/* Language and Environment */
    "target": "es2022",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "lib": ["DOM", "ES2022"],                            /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    /* Modules */
    "module": "ES2022",                                /* Specify what module code is generated. */
    "moduleResolution": "Node",

And add the module type to your ./package.json file to be able to execute the generated `js`` file inside node:

...
"type": "module",
...

Also make sure that all your module imports into the index.ts file have the js extension:

./src/modules/greeting.ts

export const greeting = (name: string = 'World'): string => {
    return `Hello ${name}!`
}


_./src/index.ts_
import { greeting } from "./modules/greeting.js";

console.log(greeting())

And import the generated script as type module inside your HTML file, if needed:

_./dist/index.html_s

...
<script type="module" src="index.js"></script>
...

Type Declaration Files

Move all type declaration and interfaces into a file types/index.d.ts and import them as Types:

import type * as Types from "./types/index";

The types can now be used as:

type AllProducts = Types.SoftKind | Types.HardKind

External Libraries

Axios comes with an index.d.ts type declaration and can be used in Typescript directly:

npm init -y
npm install axios

An example for a JSON API we can send requests to:

curl https://jsonplaceholder.typicode.com/users/1

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

We can add an type interface to ./types/index.d.ts for our Axios GET request:

export interface UserGeo {
    lat: string
    lng: string
}

export interface UserAddress {
    street: string
    suite: string
    city: string
    zipcode: string
    geo: UserGeo
}

export interface UserCompany {
    name: string
    catchPhrase: string
    bs: string
}

export interface UserAPI {
    id: number
    name: string,
    username: string,
    email: string,
    address: UserAddress
    phone: string,
    website: string,
    company: UserCompany
}

Now we can use Axios to make the request and work with the API response:

import axios from 'axios'

let apiurl: string = 'https://jsonplaceholder.typicode.com/users'

axios.get<Types.UserAPI[]>(apiurl).then((res) => {
    res.data.forEach(printUser)
}).catch(e => {
    return console.log('ERROR :: API GET request not successful!')
})


function printUser(user: Types.UserAPI) {
    const userArray: string[] = []
    userArray.push(
        String(user.id),
        user.name,
        user.email
    )
    console.log(userArray)
}

A library that does not have types included is e.g. lodash where types have to be installed manually:

npm install lodash
npm install --save-dev @types/lodash