Typescript 2023
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