DEV Community

loading...
Cover image for First-fiddle on TypeScript

First-fiddle on TypeScript

fentybit
full stack SWE at RE/MAX | former architect | flatiron school alumn
ใƒป6 min read

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
TypeScript syntax 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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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 '{}'
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)