20
A Type System
TypeScript in my opinion will always remain a superhero large scale application development tool, TypeScript comes loaded with a great type system and in this article i am going to write about arguably one of the biggest features of TypeScript which is the Type System.
This is one of the first questions i asked myself when i stumbled accross TypeScript, if valid JavaScript is TypeScript why bother adding extra codes to my already existing code base, now i see the importance of using TypeScript and i don't see myself working on a large scale application and using JavaScript, that's not just possible.
A Type system is there because it gives meaning to our code. A Type System coerces some form of sense and orderliness to our code. In JavaScript we can pass invalid types as arguments to a function. We can pass less number of arguments to the function or even more arguments than is required and all that will be fine until runtime. But you are working for an institution that is paying you hard earned dollars and time is money. How bout we catch this types of bugs before runtime? Enter a Type system.
A Type system goal is to provide a type definition for each variable, function, class, object. The types defined for each is used at compile time to perform some checks to ensure that each value assigned to a variable is of the type annotated with variable it is assigned to and if not, to expose the errors relating to wrong type of value that is passed to a variable.
TypeScript is statically typed because unlike JavaScript that performs this checks during runtime, the TypeScript compiler will actually perform this check when we try to compile our TypeScript code to JavaScript even as we are writing our code the TypeScript compiler is actually doing it's work and will notify us when we try to do something that's not valid.
TypeScript provides types for all primitive values in JavaScript as we have seen from earlier articles. Functions can also be typed but instead they are called signatures.
A function signature specifies the number and type of arguments the function can accept. It also specifies the return type of the function.
// STRING
let name: string,
name='sam' // OR name = "sam"
// NUMBER
let age: number
age = 201 //
// BOOLEAN
let isOnline: boolean
// function signature
let sayHello: (person: string) => string
sayHello can only accept a string and must return a string otherwise there will be a compile error.
Arrays are central to working with JavaScript and thus TypeScript also allows for type annotations with arrays.
// STRING ARRAY
let names: string[] = ['becker', 'ahmed', 'james']
names = [1, false, 17] // Not Okay
// NUMBER ARRAY
let prices: number[] = [1, 11, 7]
prices = ['shoes'] // Not Okay
To gain more control over what element occupies a particular index in an array we TypeScript provides tuples. A tuple is a kind of array where each index of the array can only store a particular type of value.
// TUPLES
let arr :[number, string, boolean]
arr = [1, 'becker', true] // Okay
arr = [false, 'becker', 1] // Not Okay
The array above can only store a number in it's first index, a string in it's second index and a boolean in the third index. Tuples are quite good when using the rest operator.
We can use interfaces to define the structure of an object or the shape of a class, or to combine multiple type definitions into a single type, an example of an interface is presented below;
interface Car {
wheels: number,
color: string,
plateNumber: string,
manufacturer: string,
model: string
}
// Okay satisfies the contract
let lambo: Car = {
wheels: 4,
color: 'red',
plateNumber: '234RE2',
manufacturer: 'Lamborghini',
model: 'sesto elemento'
}
// Not okay must satisfy the contract
let randCar : Car = {
wheels: '2',
plateNo: 23424,
}
Typescript also provide type alias for creating custom types and union types. Union types are for annotating variables that can store more than one type of value. While custom types allow us to create our own types from a primitive type or another type we created. We can also use literal values for type definition. When we do that any variable whose type or function whose signature accepts or returns that type will all deal with the literal value.
// TYPE ALIAS
type color: = 'red'
// COMBINING WITH UNION TYPES
type carColor = 'red' | 'green' | 'blue' | 'yellow'
// UNION TYPES
let plateNumber: string | number
let lamboColor:carColor = 'red' // Okay
lamboColor = 'purple' // Not Okay
TypeScript type system originated from the type theory developed by Bertrand Russell who developed the theory in the early 20th century. The type theory is a system where each term is given a type and operations are restricted based on the types, if we pull up a comparison between TypeScript's type annotation and the type theory we will find a great detail of striking similarity.
// TYPE THEORY
z: nat
clickZ: nat -> nat
This is a basic example of type theory building blocks, let's take a look at type annotations in TypeScript.
//TYPESCRIPT'S TYPE ANNOTATION
let num: number
let logNum: (num: number) => number;
You see the similarity i spoke about earlier? Let's go on to discuss some attributes of TypeScripts type system.
TypeScript is the product of the lessons learned from working with strongly typed languages like java and C#. So TypeScript comes with the benefit of optional typing. Plus TypeScript is a superset of JavaScript, we all know that JavaScript is dynamically types. Although this is not too good, but it comes with some benefits. Rather than being in a spaghetti like situation where you feel like you are typing your self to death. You can tell the TypeScript compiler to come of easy with the types because you don't know the actual type the variable will hold untill you assign a value to it. This can be a huge breather and gives you a sense of freedom and being in control.
// When we know the type of a value
let name: string = 'supes'
// When we don't know the type of value a hero will hold
let hero: any
hero = 'superman'
// OR
hero = {name}
// OR
hero = true
// OR
hero = 3
Incase you feel confused about the shape of your object or the type of value it should store, just annotate it with any
and you can work much like you do in JavaScript.
Another cool feature of the Type system employed by TypeScript is that if you don't specify the type for a variable, TypeScript will automatically infer the type of the value you pass to variable to it. And it leans towards making our code short and more clean especially if you assigning a value to a variable immediately after it is created. You don't actually need to annotate the variable with the type because that is really redundant.
//INSTEAD OF
let name: string = 'supes'
//RATHER USE
let job = 'coding'
let age = 20
// TypeScript will auto infer the string type to job
// and number to age
job = 600 // Not okay
age = false // Not okay
If you plan to write code that is like above where you do things the JavaScript way, remember to annotate the variable with the any
type.
Unlike the early strongly typed language that uses a nominal typing system, TypeScript uses a structural typing system. But wait what is a structural typing system and what is a nominal typing system? In nominal typing system a variable is only of a valid type when we explicitly decorate the variable definition with that type.
Let's take a use case, we know that admin on a platform must be a user. In a nominal typing system an admin is not a user and only an admin. We have to explicitly decorate it with the interface for an admin for it to be valid. This kind of system prevents situation where an object with similar properties of an admin can be valid just because it looks like it. This is cool but i don't like this approach personally. And that's where structural Typing comes in to play.
Structural Typing system is actually concerned with the internal structure of an object, that is to say as far as an admin and a user has the same structure, a user is as valid as an admin. This kind of effect with structural typing is actually desired in TypeScript. We can also achieve the same result that a nominal typing system gives us with TypeScript. Let see TypeScript's structural typing system in play
type user = {
name: string,
id: string
}
let sayHello : (obj: user) => string
let sam: user = {
name: 'sam',
id: '1'
}
let superAdmin = {
name: 'super',
id: '11'
}
sayHello = obj:user => return `${obj.name} says hello`;
// VALID
console.log(sayHello(sam)) // sam says hello
// VALID
console.log(sayHello(superAdmin)) // super says hello
If we wanted to achieve the nominal typing effect we can make use of generics, let's see a typical implementation
type userId = 'user'
type adminId = 'admin'
type user<uid extends string> = {
name: string,
id: uid
}
let sayHello: (obj: user<userId>) => string
let sam:user<userId> = {
name: 'sam',
id: 'user'
}
let superAdmin = {
name: 'super',
id: 'admin'
}
// POSSIBLE
console.log(sayHello(sam)) // sam
// NOT POSSIBLE
conosle.log(sayHello(superAdmin))
// Will show error in IDE
One thing TypeScript does that makes our work much easier is type checking. Once we have defined the types for our variables TypeScript automatically go through each assignment in our code to ensure that for each variable defined the right type of value is assigned to it. For each function the right type of arguments are called with the function. It will also ensure that the function receives the right number of arguments.
let callPerson: (phoneNo: number) => string
callPerson = (phoneNo) => `calling ${phoneNo}...`
let callKala = callPerson(234804568890); // Okay
let callFrank = callPerson('234804568890') // Not Okay
callKala = 23 // Not Okay coz callKala is a string, type inference
As we work with more complex objects and type definitions, TypeScript will test each property on each object. It will even check that each class has the right type of access modifiers for properties and in turn that they are expecting the same type and that they actually receive the right type of value. If the Object contains nested within it another object the same level of type checking will be performed on the object.
A widened type is a typical situation of a function call that returns null
or undefined
. An expression that returns either of the two also fits into
this category. And an assignment whose type is null.
let log = () => null
let widened = log()
When we compile our TypeScript code to JavaScript, the compiler will erase all type definitions, function signatures and interfaces from the compiled JavaScript code. This is because JavaScript as we know it does not support types.
That's it, hope you enjoyed it and found it useful. Personally my experience working with typescript has been superb, stay tuned for articles on TypeScript.
20