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