Joy TypeScript

10b88c68-typescript-logo

This is JavaScript — yet it’s using TypeScript under the hood.

TypeScript isn’t just types — it’s popular because this is the kind of developer experience that every other developer is used to in other languages.

What is TypeScript

TypeScript is JavaScript with syntax for types. TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. 1

The original JavaScript dose not support types. If we try to do so, we will get an error like this:

2023-01-24 at 15.18.13

Static Typing or Type checking is a feature in programming languages and recommonded by many experienced developers and development teams, where data type of a variable is checked at compile-time rather than at runtime. This means that the type of a variable must be expilicitly declared or inferred before it can be used, and any attempts to use the variable with an incompatible type will result in ca compile time error.

The opposite of static typing is dynamic typing, where the data type of a variable is determined at runtime.

Static typing can help to catch errors early in the development process, as any type mismatches will be identified during compilation, rather than at runtime when the program is being executed. This can make the debugging process easier and more efficient. Additionally, static typing can also provide better documentation and understanding of the code, as the types of variables and functions are clearly defined.

Many languages supports static typing, even with python, more and more people adopting static typing, and it made many awesome libraries like FastAPI and Pydantic and many more….

Here are some of the programming languages that support static type checking, including but not limited to:

  • C++
  • C#
  • Haskell
  • Java
  • Kotlin
  • Pascal
  • Scala
  • TypeScript
  • Python * (Note, earlier versions of python dose not support static typing,PEP484)

Before Python introduced typing, the best way you can statically check the type of your variables and data was to relying on IDEs like PyCharm, Pycharm was a great tool for static analysis of your code and providing awesome developer experience. After python introduced typing, a lot of PyCharm only features, now available even for simple text editors, making development more easier and better.

Opps, just discovered that this is a post about typescript, and i talked a lot about python …..

Let’s come back and continue our topics on typescript, as we said before, typescript is superset of javascript, which means, anything run in javascript, can run in typescript environment ( except your typing problems ), and possibilty some of your older js codes might also has errors in typescript, as it requires strong typing,

2023-01-24 at 15.32.37

If we dont enable typescript, we can push a number to our array of strings without any problems, (WebStorm being smart and giving warning, but the code still run), and if we enable typescript for this docs, it will different:

If we change our filename to .ts,

const cities = ['Istanbul', 'Ankara', 'Korla']

cities.push(1)

or more better:

const cities: string[] = ['Istanbul', 'Ankara', 'Korla']

cities.push(1)

This time we will get an ==error==:

2023-01-24 at 15.39.28

Static typecheking will help us avoid many unneccessary problems.

Dose static type checking helps up from any type of error ?

NO. Static typechecking is only cheking your code before run, and makes sure that your data is typed, there is no error with typing, it wont help you many other type of programming errors, like logical errors and many more.

Why Use TypeScript

Managing unexpected data at runtime in Typescript - Tech Shaadi

I guess we have already talked about this, cause typescript can reduce the amount of bugs we have and discover them before we deploy our production code.

Early discovering bugs are always much better than crashing our deployed production.

2023-01-24 at 18.08.06

If we use js, js wont know what attributes there for our person obj.

Runtime and Compile Time

TypeScript is JavaScript’s runtime with a compile time type checker. — TypeScript Handbook

  • Runtime is when JavaScript code gets executed
  • Compile time is when TypeScript code gets compiled to JavaScript code

TypeScript only checks your code at compile time.

This means typescript will not able to check your code errors and bugs about user input (at that time it is already deployed and running)

const person = {
    name: 'John',
    age: 30,
    address: {
        city: 'New York',
        state: 'NY'
    }
}
const people = []

function addPerson() {
    people.push(person)
}


addPerson()

2023-01-24 at 18.18.04

const animals: string[] = []

function addAnimal(animalName: string) {
    animals.push(animalName)
}

addAnimal("Cat")
addAnimal("Dog")

console.log(animals)

JavaScript Types

A primitive type is data that is not an object and has no methods.

JavaScript has 7 primitive types:

  • string — sequence of characters

  • number — floating point is the only number type
  • bigint — for huge intergers
  • boolean — logical data type with two values, true or false
  • undefined — assigned to variables that have just been declared
  • symbol — unique values
  • null — points to a nonexistent object or address

Alongside those primitive types there are primitive wrapper objects:

  • String — for the string primitive
  • Number — for the number primitive
  • BigInt — for bigint
  • Boolean
  • Symbol
❯ node
Welcome to Node.js v19.4.0.
Type ".help" for more information.
> const stringPrimitive = "Azat"
undefined
> stringPrimitive
'Azat'
> const stringObject = new String("Azat")
undefined
> stringObject
[String: 'Azat']
> stringObject.valueOf
[Function: valueOf]
> stringObject.valueOf()
'Azat'
> 

2023-01-24 at 18.40.29

JavaScript converts primitive types to primitive wrapper objects behind the scenes so we can use their methods.

Primitive wrapper objects —– we can think them as a Class.

Notice: do not confuse between the new String() constructor with the String() function:

const number = 3.14

const string = String(number)

console.log(string)

2023-01-24 at 21.38.29

By using types we also get more informations from our IDE.

When writing JS we have to consider a lot of things2:

  • Is the function callable?
  • Dose the function return anything ?
  • What are the arguments of the function?
  • What date format dose the argument accept ?
const people = []

function addPeople(name: string, timeAdded: Date) {
    people.push({
        name: name,
        timeAdded: timeAdded
    })
}

addPeople('Azat', new Date())

console.log(people)

The Date function returns a string, but we have to pass the new Date constructor that returns an object.

Type Inference

TypeScript can infer types to provide type information.

TypeScript can also infer the return type of a function. If it doesn’t return anything it’s void.

function returnName(name: string) {
    return name
        .replace(/[^a-zA-Z0-9]/g, '')
        .replace(/\s+/g, '-')
        .toUpperCase();
}

function logName(name: string) {
    console.log(name)
}

let rn = returnName('Azat')
console.log(rn)

logName('Erke')
const API: string = "https://pokeapi.co/api/v2/pokemon/"

async function getPokemonByName(name: string) {
    const response = await fetch(`${API}${name}`);
    return await response.json();
}

// show the received data

getPokemonByName('pikachu').then(data => {
    console.log(data)
}).catch(err => {
    console.log(err)
})
const API: string = "https://pokeapi.co/api/v2/pokemon/"

async function getPokemonByName(name: string): Promise<{ id: number, name: string }> {
    const response = await fetch(`${API}${name}`);
    return await response.json();
}

// show the received data

async function logPokemon(name: string) {
    const pokemon = await getPokemonByName(name);
    console.log({
        id: pokemon.id,
        name: pokemon.name,
    });
}

logPokemon('pikachu').then(() => {
});

2023-01-25 at 11.24.37

TypeScript extended types

  • any
  • unknown
  • void
  • never

Any

  • Type any represents all possible values
  • You get no type checking, so avoid using it
const apiResponse: any = {
    data: []
}

// we don't get any warning 😱
apiResponse.doesntExist

Unknown

Type unknown is the type-safe version of any:

  • Type unknown can only be assigned to type unknown and type any
  • You must use checks to type narrow a value before you can use a it
// @ts-ignore
const apiResponse: unknown = {
    data: []
}

const anyType: any = apiResponse

const unknownType: unknown = apiResponse

if (apiResponse && typeof apiResponse === 'object') {
    const response = apiResponse as { data: [] }
    response.data
}

unknown is safer to use than any when we don’t know the function argument, so instead of being able to do anything inside prettyPrint we have to narrow the type of the input argument first to be able to use it.

Void

Type void is the absence of having any type.

There’s no point assigning void to a variable since only type undefined is assignable to type void.

You mostly see type void used on functions that don’t return anything.

function logPokemon(pokemon: string): void {
  console.log(pokemon)
}

logPokemon('Pikachu') // 'Pikachu'

Let’s learn how type void is useful when used in a forEachimplementation.

function forEach(
    arr: any[],
    callback: (arg: any, index?: number) => void,
): void {
    for (let i = 0; i < arr.length; i++) {
        callback(arr[i], i);
    }
}


forEach(
    ['Astana', 'Ankara', 'Istanbul'],
    (name: string) => console.log(name),
)

2023-01-25 at 11.44.10

Using the return type void explicity can save us from returning a value on accident during refactor.

Never

Type never represents values that never occur:

  • Type never can’t have a value
  • Variables get the type never when narrowed by type guards to remove possibilities (a great example is preventing impossible states when a prop is passed to a component where you can say if one type of prop gets passed another can’t)

Type unknown can be used together with type narrowing to ensure we have a check for each Pokemon type.

function getPokemonByType(
  pokemonType: 'fire' | 'water' | 'electric'
) {
  // type is 'fire' | 'water' | 'electric'
  if (pokemonType === 'fire') {
    return '🔥 Fire Pokemon'
  }

  // we narrow it down to 'water' | 'electric'
  if (pokemonType === 'water') {
    return '🌀 Water Pokemon'
  }

  // only 'electric' is left
  pokemonType

  // remainingPokemonTypes can't have any value
  // because pokemonType is 'electric' 🚫
  const remainingPokemonTypes: never = pokemonType

  return remainingPokemonTypes
}

getPokemonByType('electric')

Array Types

To specify an array type you can use the generics Array<Type> syntax or the Type[] syntax.

Function Types

You can specify the input and output type of functions.

const pokemon: string[] = []

function addPokemon(name: string): string[] {
  pokemon.push(name)
  return pokemon
}

addPokemon('Pikachu')

Function Overloads

Function overloading is the ability to create multiple functions of the same name with different implementations. Which implementation gets used depends on the arguments you pass in.

In JavaScript there is no function overloading because we can pass anynumber of parameters of any type we then perform checks on inside the function.

function logPokemon(arg1, arg2) {
    if (typeof arg1 === 'string' && typeof arg2 === 'number') {
        console.log(`${arg1} has ${arg2} HP.`);
    }
    if (typeof arg1 === 'object') {
        const {name, hp} = arg1
        console.log(`${name} has ${hp} HP.`);
    }
}

logPokemon('Pikachu', 100);
logPokemon({name: 'Pikachu', hp: 34})

2023-01-25 at 11.53.03

interface Pokemon {
    name: string,
    hp: number,
}

function logPokemon(name: string, hp: number): void;
function logPokemon(pokemonObj: Pokemon): void;


function logPokemon(arg1: unknown, arg2?: unknown): void {
    // matches the first overload signature
    if (typeof arg1 === 'string' && typeof arg2 === 'number') {
        console.log(`${arg1} has ${arg2} HP.`);
    }

    // matches the second overload signature
    if (typeof arg1 === 'object') {
        // since it's an object we can assert the type to be Pokémon
        const {name, hp} = arg1 as Pokemon
        // log the destructured values
        console.log(`${name} has ${hp} HP.`)
    }
}

// 'Pikachu has 35 HP.' ✅
logPokemon('Pikachu', 35)

// 'Pikachu has 35 HP.' ✅
logPokemon({ name: 'Pikachu', hp: 35 })

2023-01-25 at 11.58.56

Note that arg2 is optional.

Object Types

2023-01-25 at 12.37.43

It will work after we make id?

const pokemonMissingProperty: { id?: number, name: string } = {
    name: 'Pikachu'
}

2023-01-25 at 12.39.38

Type Alias

A type alias is as the name suggests — just an alias for a type.

type Pokemon = {
    id: number;
    name: string;
    pokemonType: string;
}

const pokemons: Pokemon[] = [
    {
        id: 1,
        name: 'bulbasaur',
        pokemonType: 'electric'
    }
]


pokemons.forEach((pokemon) => console.log(pokemon))

2023-01-25 at 12.45.20

type PokemonNew = string[] | string

function logPokemon(pokemon: PokemonNew): PokemonNew {
    // pokemon is an array of string
    if (Array.isArray(pokemon)) {
        return pokemon.map(p => p.toUpperCase())
    }

    if (typeof pokemon === 'string') {
        return pokemon.toUpperCase()
    }

    return 'Please enter a valid Pokemon!'
}

console.log(logPokemon(['First', 'Second', 'Third']))
console.log(logPokemon('Pikachu'))
console.log(logPokemon(''))

2023-01-25 at 12.50.49

type logPok = (pokemon: string) => void

// named function expression

const logPokemon1: logPok = function logPok(pokemon) {
    console.log(pokemon)
}

// anonymous function expression
const logPokemon2: logPok = function (pokemon) {
    console.log(pokemon)
}

// anonymous arrow function expression

const logPokemon3: logPok = (pokemon) => console.log(pokemon)


logPokemon1('A')
logPokemon2('B')
logPokemon3('C')

2023-01-25 at 12.55.06

type Pokemon4 = {
    name: string;
    pokemonType: string
}

type PokemonConstructor = {
    new(name: string, pokemonType: string): Pokemon4
}

class PokemonFactory implements Pokemon4 {
    name: string;
    pokemonType: string;

    constructor(name: string, pokemonType: string) {
        this.name = name;
        this.pokemonType = pokemonType;
    }
}

function addPokemon4(
    pokemonConstructor: PokemonConstructor,
    name: string,
    pokemonType: string
): Pokemon4 {
    return new pokemonConstructor(name, pokemonType);
}

const pokemon4: Pokemon4 = addPokemon4(
    PokemonFactory,
    'Pikachu',
    'electric'
)


console.log(pokemon4)

2023-01-25 at 13.00.03

Interfaces

Interfaces are another way to name an object type.

// Interfaces are another way to name an object type.

interface Pokemon {
    id?: number,
    name: string,
    pokemonType: string,
    ability: string,

    attack(): void
}

const pokemon: Pokemon[] = [
    {
        id: 1,
        name: 'Bulbasaur',
        pokemonType: 'grass',
        ability: 'overgrow',

        attack() {
            console.log(`${this.name} used ${this.ability}.`)
        }
    },
    {
        id: 2,
        name: 'Pikachu',
        pokemonType: 'electric',
        ability: 'aaa',

        attack() {
            console.log(`${this.name} used ${this.ability}.`)
        }
    }
]

pokemon[0].attack()
pokemon.forEach(pokemon => pokemon.attack())

2023-01-25 at 13.12.53

Inside the interface we can say properties are optional using ? and type function signatures like attack(): void.

  • Interface is more appropriate for describing shapes of objects
  • You can add new fields to an existing interface but not to a type alias

We haven’t learned about intersections yet but briefly it just lets us combinetypes.

2023-01-25 at 13.16.20

type Pokemon1 = {
    id: number,
    name: string
}

type Electric = Pokemon1 & { pokemonType: 'electric' }

// has to satisfy the same checks ✅
const pikachu: Electric = {
    id: 1,
    name: 'Pikachu',
    pokemonType: 'electric'
}

console.log(pikachu)

2023-01-25 at 13.16.44

In the case of interfaces we use the extends keyword to extend them.

interface PokemonInterface {
    id: number,
    name: string
}

interface ElectricInterface extends PokemonInterface {
    pokemonType: 'electric'
}

const pikachu2: ElectricInterface = {
    id: 1,
    name: 'Pikachu',
    pokemonType: 'electric'
}


console.log(pikachu2)

2023-01-25 at 13.18.47

When using an interface you should always keep in mind that you can use an existing interface which could lead to some unexpected results.

‘We might unexpectedly rewrite a global interface’.

Union Type

A union type is a type made from at least two types and represents anyvalues of those types.

function logPokemon(pokemon: string[] | string) {
    console.log(pokemon);
}

logPokemon(['a', 'b', 'c'])
logPokemon('pikachu')

The pokemon argument can only be an array of Pokemon of type string[] or a single Pokemon of type string.

function logPokemon(pokemon: string[] | string) {
  if (Array.isArray(pokemon)) {
    // `pokemon` can only be an array ✅
    console.log(pokemon.map(pokemon => pokemon.toUpperCase()))
  }

  if (typeof pokemon === 'string') {
    // `pokemon` can only be string ✅
    console.log(pokemon.toUpperCase())
  }
}

// ['BULBASAUR', 'CHARMANDER', 'SQUIRTLE']
logPokemon(['Bulbasaur', 'Charmander', 'Squirtle'])

// PIKACHU
logPokemon('Pikachu')

function logPokemon(pokemon: string[] | string) {
    if (Array.isArray(pokemon)) {
        console.log(pokemon.map(p => p.toUpperCase()));
    }
    if (typeof pokemon === 'string') {
        console.log(pokemon.toUpperCase());
    }
}

logPokemon(['a', 'b', 'c'])
logPokemon('pikachu')

2023-01-25 at 21.45.26

Discriminated Unions

A discriminated union is the result of narrowing the members of the union that have the same literal type.

For example we can use a type guard to narrow the Pokemon type 'fire' | 'water' to 'fire' or 'water'.

interface Pokemon {
    flamethrower?: () => void
    whirlpool?: () => void
    pokemonType: 'fire' | 'water'
}

function pokemonAttack(pokemon: Pokemon) {
    switch (pokemon.pokemonType) {
        case 'fire':
            pokemon.flamethrower();
            break;
        case "water":
            pokemon.whirlpool();
            break;
    }
}

// 🔥 'Flamethrower'
pokemonAttack({
    pokemonType: 'fire',
    flamethrower: () => console.log('🔥 Flamethrower')
})

// 🌀 'Whirlpool'
pokemonAttack({
    pokemonType: 'water',
    whirlpool: () => console.log('🌀 Whirlpool')
})

Much better version will be:

interface Fire {
  flamethrower: () => void
  pokemonType: 'fire'
}

interface Water {
  whirlpool: () => void
  pokemonType: 'water'
}

type Pokemon = Fire | Water

function pokemonAttack(pokemon: Pokemon) {
  switch (pokemon.pokemonType) {
    case 'fire':
      pokemon.flamethrower() // ✅
      break
    case 'water':
      pokemon.whirlpool() // ✅
      break
  }
}

// 🔥 'Flamethrower'
pokemonAttack({
  pokemonType: 'fire',
  flamethrower: () => console.log('🔥 Flamethrower')
})

// 🌀 'Whirlpool'
pokemonAttack({
  pokemonType: 'water',
  whirlpool: () => console.log('🌀 Whirlpool')
})

Intersection Types

Intersection types let us combine types using the & operator.

The next example also shows how we can use interfaces and type aliasestogether.

interface Pokemon2 {
    name: string;
    gp: number
    pokemonType: [string, string?]
}

interface Ability {
    blaze(): void
}

interface Moves {
    firePunch(): void
}

type Fire = Ability & Moves
type FirePokemon = Pokemon2 & Fire

const charizard: FirePokemon = {
    name: "Charizard",
    gp: 100,
    pokemonType: ["fire", "flying"],
    blaze() {
        console.log(`${this.name} used 🔥 Blaze.`)
    },
    firePunch() {
        console.log(`${this.name} used 🔥 Fire Punch.`)
    }
}

charizard.blaze() // 'Charizard used 🔥 Blaze.'
charizard.firePunch() // 'Charizard used 🔥 Fire Punch.'

2023-01-25 at 21.56.26

If we wanted to use an interface we could and it works just the same.

interface Fire extends Ability, Moves {}

interface FirePokemon extends Pokemon, Fire {}

Type Assertion

Type assertion is like type casting in TypeScript where it can be used to specify or override another type.

const formEl = document.getElementById('form') as HTMLFormElement

formEl?.reset() // works ✅

There’s also the alternative angle bracket <> syntax for type assertion.

const formEl = <HTMLFormElement>document.getElementById('form')

formEl?.reset() // works ✅

Event listeners are a big part of JavaScript and something we use often even in JavaScript frameworks.

In the next example we have an input field with an event listener that takes a Pokemon name.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Example TypeAssertion</title>
</head>
<body>
<label for="pokemon">input</label><input type="text" id="pokemon">
<script src="main.js"></script>
</body>
</html>
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement


function handleInput(event) {
    console.log(event)
}

pokemonInputEl.addEventListener('input', (event) => handleInput(event))
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "sourceMap": true,
    "allowJs": true
  },
  "exclude": [
    "node_modules"
  ]
}
npx tsc ./src/11-type-assertion/main.ts
const pokemonInputEl = document.getElementById('pokemon') as HTMLInputElement

function handleInput(event: Event) {
  const targetEl = event.target! as HTMLInputElement
  targetEl.value
}

pokemonInputEl.addEventListener('input', (event) => handleInput(event))

Type Assertion Using !

  1. https://www.typescriptlang.org/ 

  2. https://joyofcode.xyz/typescript-fundamentals