Six months of Typescript

20 Jul,2020 | 6 min read

It's been six months since I started using Typescript in my projects. I've learned a lot about Typescript, its strength and its quirks. I'll provide an overview of the features I've appreciated and share tips to avoid common pitfalls.

Union Types

Union Types reminds of the OR operator, it represents a type that can be one of several types. Let's see it in a example

type Currency = "USD" | "EUR" | "GBP";

Any variable that assigned to type Currency, can be only one of those threes. Thus decreasing the chance of accidentally assigning the wrong value.

Intersection Types

An intersection represents a type that has all the properties of two or more types.

Let's say we have an Author type, and an Admin type. We want to create a User type using these two

type Author = { name: string; group: string };
type Admin = { name: string; privilege: string[] };
type User = Author & Admin; //{ name: string, group: string, privilege: string[]}

Also make the types we use don't have common properties with different types.

type A = { points: string };
type B = { points: number };
type C = A & B; //Will throw an error since points properties have different types.

This type is really useful when working with React. by combining your custom props with element predefined ones.

type Props = {
color: string;
};
const Button: React.FC<Props & React.HTMLAttributes<HTMLButtonElement>> = ({
children,
style,
color,
...props
}) => (
<button style={{ ...style, color }} {...props}>
{children}
</button>
);
// later
<Button color="red" onClick={() => {}}>
Click Here
</Button>;

Never

As defined in the Typescript docs, the never type represents the type of values that never occur.

From the definition, Never type indicate that a value never occurs, not even undefined.

let's see some examples

const OneFunction = (param):never=> { // Will throw an error, since it returns undefined
console.log(22) // But we said it should never return.
}
const SecondFunction = (param):never => {
while(true){ // Now you see we never returns anything.
}
}
const Third Function = (param):never => {
throw new Error('Shit happens man.')
}

We can notice two patterns from the above example. We should use never type if the function continues to run forever or if it breaks the execution. We can use never to write custom error handlers.

Type Assertion

With type assertion, we tell Typescript, this is the type of this variable you don't need to infer it.

let a: any = 213; // this is assigned to 'any' type.
let b: number = a as number; // using the as keyword, we can assert typescript
// is for sure a number

We can also use type assertion to make a variable immutable.

let a = { name: "alex", age: 10 };
a.name = "Jhon"; // expected behavior
let b = { name: "alex", age: 10 } as const;
b.name = "jhon"; //Typescript compiler will yell at you for mutating it.

Unknown

Unknow is a better solution than any. Unknown typed variables can't be assigned to anything, but we can assign it to other variables.

let a: unknown;
let b: number = 5;
a = b; // works.
b = a; // Throws an error.

We can handle the unknown type by two ways, narrowing it or with type assertion.

let a: unknown;
let b: number = 5;
if (typeof a === "number") {
b = a; // it works
}
b = a as number; // it works too.

Mapped Types

The mapped type takes an existing type and maps its properties to a different type.

const Type<T> = { [P in keyof T]: P }
//Keys //Types

I'd like to read it as for every P (property) in keyof T (Properties of Type T) : give it this type.

The keyof operator indicates that we're looking for all properties of type T.

We can use Mapped types for all magical stuff, we can create a Partial Type, Required Type, and lot more.

type Partial<T> = {
[P in keyof T]?: T[P];
// Keys of T type of each property
};
type Required<T> = {
[P in keyof T]: T[P];
// Keys of T type of each property
// The absense of ? means that every property is required in this Type.
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
// P = Properties Selected assigned to generic variable called K
};
// Examples
type User = {
name: string;
email: string;
age?: number;
};
let user: User = { name: "Alex", email: "example@test.com" }; // Works fine.
let partialUser: Partial<User> = {}; // Works fine, since all properties are not required.
let requiredUser: Required<User> = { name: "Alex" }; // throws an error, name and age are required.
requiredUser = { name: "Alex", email: "example@test.com", age: 12 }; // Works fine.
let userContactInfo: Pick<User, "name" | "email"> = { age: 23 }; // Error. We only picked name and email properties.
userContactInfo = { name: "Alex", email: "example@test.com" };

Conditional Types

Conditional type can a bit tricky at first, it's quite difficult to see a use case for it. Let's see a problem that it fixes.

We have a function that takes either a string or null and return string or null. If we pass string we want it to return a string, otherwise null. It seems quite intuitive for Typescript to infer it.

const toLowerCase = (str: string | null): string | null => {
return str ? str.toLowerCase() : null;
};
const name = toLowerCase("Alex"); // name is a string or null

It was surprising at first this didn't work not gonna lie,.

Anyway, this is a great problem where conditional types shine.

const toLowerCase = <T extends string | null>(
str: T
): T extends string ? string : null => {
return str ? str.toLowerCase() : null;
};
//Throws an error
//Type 'string' is not assignable to type 'T extends string ? string : null'.
const name = toLowerCase("Alex");

But it didn't work, I know it's disappointing apparently TS can't type-check the return value of functions with conditional return types defined in terms of type parameters.

For this use-case, we can work with function overload.

function toLowerCase(str: null): null;
function toLowerCase(str: string): string;
function toLowerCase(str: string | null): string | null {
return str ? str.toLowerCase() : null;
}
const res = toLowerCase(null); //null
const res = toLowerCase("Alex"); //string

Type Guards

Type Guards comes handy when you want to narrow a type, you probably already know the typeof operator in Javascript. Its actually used as type guard as well when using Typescript.

const logString = (a: unknown): void => {
if (typeof a === "string") {
console.log(a); // a is infered as string thanks to the guard we wrote.
}
};

we can also define our custom type guard.

type Car = {
engine: string;
};
const tesla = {
engine: "electric",
};
const isCar = (car: Car): car is Car => {
return typeof car.engine === "string";
};
if (isCar(tesla)) {
console.log(tesla.engine); // 'electric' and tesla is a car in this block.
}

Derive Types from Constants

By defining a const variable, we can derive types from it. We have an Icon system, we can easily derive its type thus decreasing the overload of updating the type each type a new icon is added

const Icons = {
twitter: ()=>//..,
github: ()=>//..
devto: ()=>//..
} as const
type Icon = keyof typeof Icons // "twitter" | "github" | "devto";

2020

Six months of Typescript

20 Jul