Some Advanced Typescript Features

Rajeev Singh
HeyJobs Tech
Published in
6 min readSep 8, 2022
Image by optics.jpg

We all have used some kind of type system in our code, here I will be talking about TypeScript which is a strongly typed programming language that trans-piles to JavaScript.

Over the last few years, it has evolved into more than just static types and has become what I like to call intelligent typing. It has a very powerful type system because it allows expressing types in terms of other types, can infer types from the program logic and by that can catch more type issues.

This article is mainly about the advanced features of TypeScript & its usage.

If you haven’t played with Typescript yet I would suggest you check out the TS Playground

Do you have a lot of redundant types? Do you want to make your typing more reusable? Do you want more type-safety? And in turn, writing more maintainable code? Below I’ll list some advanced typescript features that will help you achieve this.

The keyof type operator

The keyof operator takes an object type and produces a string or a numeric literal union of its keys.

The following type of Coordinates is the same type as “x” | “y”:

type Point = { x: number; y: number };type Coordinates = keyof Point; // ‘x’ | ‘y’

This can have many use cases for example the most obvious one is to make sure you only pass the valid keys while accessing the Object’s property

function getCoordinate(coordinate: keyof Point, point: Point) {              
return point[coordinate]
}
// will give error as key 'z' doesn't exists on Point type
getCoordinate('z')

The typeof type operator

The typeof operator you can use in a type context to refer to the type of a variable or property

const evenOrOdd = (num: number) => num % 2 === 0 ? “even” : “odd”type EvenOrOdd = ReturnType<typeof evenOrOdd> // “even” | “odd”

If we make changes on evenOrOdd’s return type, the type of EvenOrOdd will follow its changes.

Indexed Access Types

We can use an indexed access type to look up a specific property on another type

type Person = {
age: number;
name: string;
alive: boolean
}
type Age = Person[“age”]; // number

In the above example, you can see we created a new type Age by accessing the ‘age’ property of the Person type, which is very similar to accessing a key value from a javascript object.

The indexing type is itself a type, so we can use unions, keyof, or other types as indexes.

type AgeOrName = Person[“age” | “name”]; // string | number// below type will resolve to string | number | boolean
type PersonPropertiesType = Person[keyof Person];

Conditional Types

Conditional types help describe the relationship between the types of inputs and outputs.

Syntax: SomeType extends OtherType ? TrueType : FalseType;

type TimeOrString = Park extends Verb ? Park[“time”] : Park[“name”]// with generics
type MessageOf<T extends { message: unknown }> = T[“message”];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;

With generic types we can specify conditions to what types are allowed to be passed to a generic type, here in the example above MessageOf type only accepts a type that extends the type { message: unknown } (has a message property).

Passing any other type will result in an error message.

// error: Property ‘message’ is missing in type ‘Person’
type PersonMessage = MessageOf<Person>;

Generics

One of the main tools in the toolbox for creating reusable components is generics. Generics enable you to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their types.

// not generic
type List = {
items: string[]
}
// bad
type List = {
items: any[] // using any type
}
function renderList(list: List) { ... } // not generic render

by using ‘any’ type you lose the type safety and expose yourself to issues that are difficult to trace, so a better option will be to use generics

type List<T> = {
items: T[]
}
function renderList<T>(list: List<T>) { … }

Mapped Types

When you don’t want to repeat yourself, sometimes a type needs to be based on another type. A mapped type is a generic type that uses a union of PropertyKeys (frequently created via a keyof) to iterate through keys to create a type.

Suppose we have a type Todos

type Todos = {
eat: Eat,
sleep: Sleep,
code: Code
}

Now we want to have another type that represents the todo’s status, so basically we have all the todos with a boolean flag to check whether the task is done or not. Which will look something like below

type TodoStatus = {
eat: boolean,
sleep: boolean,
code: boolean,
}

But the same thing can also be written using the Mapped type, which is way more concise and doesn’t need to be updated if you decide to add more properties to your Todo type.

type TodoStatus = {
[Property in keyof Todos]: boolean
}

Key Remapping via as

In TypeScript 4.1 and onwards, you can remap keys in mapped types with an as a clause in a mapped type.

type Person2 = {
name: string,
age: number,
canBackflip: boolean,
}
type MappedTypeToNew<Type> = {
[Properties in keyof Type as 'newProperty']: Type[Properties]
}

In the above example, MappedTypeToNew remaps all the keys of the passed Type to string ‘newProperty’

So if we then pass Person to MappedTypeToNew it will result in the type below, which means all keys are renamed to newProperty and then the types also are combined to have one union type of all key types “ string | number | boolean ”

{
newProperty: string | number | boolean;
}

Some more complex use cases of mapped types with ‘as’ remapping can be something like creating a mapped type with all the getter methods of an object. So our getter type can be something like

// adds a get prefix to all the properties and capitalizes the first chartype Getters<T> = {
[Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property]
}

Then if you want to create a Getter type for some type, you can do something like below

type PersonGetters = Getters<Person2>

which is equivalent to writing

type PersonGetters = {
getName: () => string;
getAge: () => number;
getCanBackflip: () => boolean;
}

Template Literal Types

They have the same syntax as template literal strings in JavaScript but are used in type positions.

type Width = `${number}px`; // will accept values like ‘1px’, ‘2px’// possible types for a chess gametype Columns = ‘a’ | ‘b’ | ‘d’ | ‘e’ | ‘f’ | ‘g’ | ‘h’type Rows = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8type Cell = `${Columns}${Rows}` // “a1” | “a2” | “a3” | “a4” | …

Outro

There are a lot more things to still explore. I like to use Utility types a lot. They cover the most common use cases that you might encounter, but the above-mentioned features are also very useful and can help you write more robust, reusable, and type-safe code.

References

Interested in joining our team? Browse our open positions or check out what we do at HeyJobs.

--

--