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:
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,
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==:
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
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.
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()
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 typebigint
— for huge intergersboolean
— logical data type with two values,true
orfalse
undefined
— assigned to variables that have just been declaredsymbol
— unique valuesnull
— points to a nonexistent object or address
Alongside those primitive types there are primitive wrapper objects:
String
— for the string primitiveNumber
— for the number primitiveBigInt
— forbigint
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'
>
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)
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(() => {
});
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 typeunknown
and typeany
- 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 forEach
implementation.
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),
)
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})
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 })
Note that arg2
is optional.
Object Types
It will work after we make id?
const pokemonMissingProperty: { id?: number, name: string } = {
name: 'Pikachu'
}
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))
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(''))
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')
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)
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())
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.
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)
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)
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')
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.'
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))