First-fiddle on TypeScript
fentybit
Posted on August 1, 2021
I have spent the past few weeks diving in to TypeScript. It has been fun! 😆 TypeScript is a superset of JavaScript, that means it carries all of the JS syntax, including some new syntax as well. Variables in TypeScript have static types. Types come after a :
.
TypeScript knows the types of built-in JavaScript functions and methods. While any code with valid syntax typically runs in JavaScript, code must have valid syntax and valid type check in order to run in TypeScript.
JavaScript
syntax check
→execution
TypeScriptsyntax check
→type check
→execution
For example in TypeScript, a + b
is a valid syntax. However, if a
is a number
and b
is a boolean
, then a + b
will not fulfill a valid type check
. This results in a type error.
Basic types
To declare our own names for types, we can use the type
keyword. It is similar to declaring a variable let
. By convention, user-defined type names are UpperCamelCased.
type CarrotCake = string;
let c: CarrotCake = 'It is delicious';
c;
// 'It is delicious'
Types are only used during type checking prior to execution. The declaration of type CarrotCake = string
is a TypeScript syntax. The technical jargon is 'type erasure'. It is solely used for type checking, and later on discarded from the compiled output. Types are not exclusive to only variables, but also to function types.
type HowManyEggsForACake = (egg: number) => string;
function redVelvetCake(egg: number): string {
return egg.toString() + ' eggs to start';
}
const f: HowManyEggsForACake = redVelvetCake;
f(2);
// '2 eggs to start'
I can implement similar de-structuring assignment on JavaScript to TypeScript.
function makeCake({cake}: {cake: string}): string {
return cake;
}
makeCake({cake: 'Sponge Cake'});
// 'Sponge Cake'
Arrays
I like the fact that TypeScript allows the use of an 'array of data' type for function arguments and function return values. In the example below, function arguments contain an array of strings.
function arrayOfCakes(cakes: string[]) {
return cakes;
}
string[]
is syntactically identical to Array<string>
. This syntax, let otherCakes: Array<string> = ['Banana bread', 'Bebinca']
, is also perfectly valid. Furthermore, I can make an 'array of arrays of data'. Example use of case as follows.
const arrayOfCakes: string[][] = [['Birthday Cake'], ['White Cake']];
// optionally, you can write (string[])[]
function cakes(namesAndCakes: [string, number][]): string[] {
return namesAndCakes.map(cake => cake[0]);
}
cakes([['Angel Cake', 3], ['Apple Cake', 1]]);
// ['Angel Cake', 'Apple Cake'];
Inference
We can certainly avoid writing types. This is called type inference, and TypeScript infers types. Inference means that the compiler determines the types for us. Types does not necessarily differ from place to place. Every type can be used anywhere that types are allowed. Such as, string
can be a type of a variable, a function argument, or a function's return value.
function cake() {
return 'cup' + 'cake';
}
cake();
// 'cupcake'
TypeScript has generic function inference, and this allows us to call function many times without specifying the type parameters. We can name our generic type parameter T
, and you can use any name you like. Type safety will still be maintained throughout code execution.
function cakeSample<T>(cakes: T[]): T {
return cakes[1];
}
let results: [boolean, string] = [
cakeSample<boolean>([true, true, false]),
cakeSample<string>(['Walnut Cake', 'Orange Cake', 'Fruit Cake']),
];
results;
// [true, 'Orange Cake']
Type error
In JavaScript, there is a common symptom of undefined
error from a function. TypeScript's object types inform back of any type errors during compilation. This helps to identify early rather than failing in production.
type Cake = {
ingredient: string;
delicious: boolean
}
let lemoncake: Cake = {
ingredient: 'lemon',
delicious: true,
}
lemoncake.delicious;
// true
let bundt: Cake = {
ingredient: 'chocolate'
}
// type error: missing { delicious: boolean } in type but required in type 'Cake'
Literal types
While we have seen basic types such as boolean
and string
, every concrete number is also a type. A variable of type 1
can only hold the number 1
. It can not hold number 2
, this is a compile-time type error. Type 1
here is a literal number type. We can combine literal types with unions to allow only certain values.
let uno: 1 = 1;
one;
// 1
let unoDos: 1 | 2 = 2;
unoDos;
// 2
type deliciousCake = 'Biscuit Cake' | 'Angel Food Cake' | 'Carrot Cake';
let aCake: deliciousCake = 'Hazelnut Mousse Cake';
aCake;
// type error: type "Hazelnut Mousse Cake" is not assignable to type 'deliciousCake'
Tuples
This is a new syntax to my TypeScript learning, tuples. They are arrays of fixed length, in which each type is defined.
let trays: [string, number] = ['Pound Cake', 2];
trays[0];
// 'Pound Cake'
let platter: [string, number] = ['Vanilla Cake'];
// type error: target requires 2
Type unions
As an extension of JavaScript, TypeScript is able to add static types to existing JavaScript code. The a | b
syntax means either type a
or type b
.
type Cake = {name: string};
function isCake(c: Cake[] | Cake): string[] {
return Array.isArray(c) ? c.map(cake => cake.name) : [cake.name];
}
isCake([{name: 'Butter Cake'}, {name: 'Chiffon Cake'}]);
// ['Butter Cake', 'Chiffon Cake']
There is a type unsoundness that I found on TypeScript. We understand that we can assign our array to a new variable of type (string | number)[]
. If an array contains only strings, this particular array of string | number
just happens to have no numbers in it right now. Our array variables have different types, but the underlying array is the same. If I were to push a number onto the strings array, weirdly TypeScript allows this. This clearly violates the deliciousCakes
variable's string[]
type!
let deliciousCakes: string[] = ['Cheesecake', 'Strawberry Cake'];
let cakeLovers: (string | number)[] = deliciousCakes;
cakeLovers.push(8);
cakeLovers;
// ['Cheesecake', 'Strawberry Cake', 8]
any
type would be another example of type unsoundness in TypeScript.
const cake: any = 'Coffee Cake';
const myCake: string = cake;
myCake;
// 'Coffee Cake'
const cake: any = 'Coffee Cake';
const yourCake: number = cake;
yourCake;
// 'Coffee Cake'
We defined cake
a string type, 'Coffee Cake'. We can put a string in an any
, then assign it to a variable of type number
. This is wrong, but it will not cause a type error. Another way to approach this would be to utilize the unknown
type. We use unknown
to represent values of which type is not known yet.
const cake: unknown = 'Coffee Cake';
const myCake: string = cake;
myCake;
// type error: Type 'cake' is not assignable to type 'string'
const cake: unknown = 'Coffee Cake';
const myCake: string = typeof cake === 'string' ? cake : 'No Cake';
myCake;
// 'Coffee Cake'
We can not use unknown
where TypeScript expects a string
or any other type. This will give you a type error. One way to make unknown
useful is to use conditional narrowing the unknown back to a string
type.
Nullish coalescing
In TypeScript, following values are equivalent of false
— false
, 0
, 0n
, ''
, undefined
, null
, and NaN
. It gets tricky when..
function numberOfCake(n: number | undefined): number {
return n || 1;
}
numberOfCake(0);
// 1
This is not fully accurate as 0
is a number too, and numberOfCake(0)
should be returning 0
. There is a new features called nullish coalescing in 2019 ECMAScript. The nullish coalescing operator is ??
, and it is similar to JavaScript logical OR operator, ||
.
1 ?? 'default' === 1
0 ?? 'default' === 0
'cake' ?? 'bananaBread' === 'cake'
'' ?? 'marbleCake' === ''
null ?? 'appleCrumble' === 'appleCrumble'
undefined ?? 'financier' === 'financier'
false ?? 'caramel' === false
function numberOfCake(n: number | undefined): number {
return n ?? 1;
}
numberOfCake(0);
// 0
Nullish coalescing does not consider 0
and ''
as falsey. It is only used for checking null
and undefined
, which means if we are getting false
, that is because false
is not null
or undefined
.
Optional chaining
Let's start with a Cake
type, and each cake has ingredients, but only sometimes. The Ingredients
type have nuts, but only sometimes. If we want to compile a list of cakes' nuts, Lamingtons
will not cause a problem since the cake's nuts is undefined
. However, Lemon Yoghurt Cake
's nuts will pose a problem. Since its ingredients is undefined
, asking for ingredients.nuts
will cause a type error.
type Cake = {
name: string
ingredients: Ingredients | undefined
};
type Ingredients = {
egg: number
nuts: string | undefined
};
const cakes: Cake[] = [
{
name: 'Walnut Cake',
ingredients: {
egg: 4,
nuts: 'walnuts',
}
},
{
name: 'Lamingtons',
ingredients: {
egg: 2,
nuts: undefined,
}
},
{
name: 'Lemon Yoghurt Cake',
ingredients: undefined,
},
];
cakes.map(cake => cake?.ingredients?.nuts);
// ['walnuts', undefined, undefined]
Optional chaining comes to the rescue, ?.
. It checks whether the object is null
or undefined
. If it is, the expression will return undefined
. If it is not, it will return the value of the object's property. With ?.
, we can securely access properties and sub-properties of an object that may be null
or undefined
. Important to note, even if it's null
, it will still return undefined
.
As
TypeScript does not allow an object type.
const cake = {};
cake.name = 'Battenberg Cake';
cake.diameter = 10;
cake;
// type error: property 'cake' does not exist on type '{}'
We can use as
to build a cake object, starting with the empty object {}
. We are able to surpass the normal type checking, and have the compiler to treat our cake as an object type {name: string, diameter: number}
.
const cake = {} as {name: string, diameter: number};
cake.name = 'Battenberg Cake';
cake.diameter = 10;
cake;
// {name: 'Battenberg Cake', diameter: 10}
as
is dangerous as it overrides the type system, and we lose this type check safety. For example, we can tell TypeScript that a number is a string. as
overrides that, and now the types are just wrong.
const cake: unknown = 1;
const aCakeString = cake as string;
aCakeString;
// 1
While my knowledge exposure to TypeScript is minimal, I am super excited to implement this new skill to a real application. I feel TypeScript gives a bit more rigidity to the liberal JavaScript. Thanks TypeScript and many cake types discovery, it's nice getting to know ya! 🍰
fentybit | GitHub | Twitter | LinkedIn
Posted on August 1, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.