Saturday, February 27, 2021

Introduction to Index Types in TypeScript

This post will look at Index Types in TypeScript. It will go over what they are and also touch on two type-operators related to them. These type operators are the index type query operator and indexed access operators. 

This post is part of the Introduction to Advanced Types in TypeScript series. To better understand, it requires having some familiarity with Union Types, Generics, and Literal types. These are topics that have already been covered in the series.

As mentioned in the introductory post to these series, a good mental model to have, when exploring the advanced part of TypeScript type system is to see it as a more sophisticated mechanism for creating types. A mechanism for constructing new types from existing types.

Index Types are an example of such capabilities. It is a mechanism TypeScript provides that allows the construction of or extraction of a type based on the type found at the index of an existing type.

Remember in JavaScript, there are two ways to access the properties of objects: These two ways are the dot notation and the bracket notation.

var person = { name: "foobar", age: 42}
person.name // dot notation
person["name"] // bracket notation

The "name" in the dot notation above can be seen as the index that is used to access that value. A similar thing is achieved with Index Type, but instead of working with values, we work with Types and we get out a type in return.

To illustrate the above statement, let us say we are given the following type:

type Employe = {
name: string
employedOn: Date
age: string
role: {
  title: string
  salary: number
  isManagement: boolean
 }
}

We can then create a new type, by extracting it from the above type. The extraction is done by specifying an index. 

For example to create a new type role from the Employee type above, you’ll have:

type Employee = {
name: string
employedOn: Date
age: string
role: {
  title: string
  salary: number
  isManagement: boolean
 }
}

type Role = Employee["role"]

Which we can then use as shown below:

let growthHacker: Role = {
 title: "Junior growth Hacker",
 salary: 1000,
 isManagement: false
}

So a quick recap, we were able to create a new type, in the above case Role, from an existing type Employee by indexing into the existing type using one of its properties; which in the above example is the role property. In essence, we referenced the type of a property found in one type using the square bracket notation.

Role is an example of Index Type.

This can come in handy in situations where we do not want to explicitly define a standalone type but content with reusing the type of a property of another type.

The same approach also works with an array since arrays are indexable, we can also reference a type based on the type found at an index of an array.

type MyArray = [string, number, boolean]
type AString = MyArray[0]
type ANumber = MyArray[1]
type ABoolean = MyArray[2]

let stringVal: AString = "abc"
let numVal: ANumber = 123
let boolVal: ABoolean = true

Note that in the two examples so far, the indexes used are literal types (string literals and number literal). TypeScript only supports Literal types as indexes. This can be confirmed as follows:

This compiles because the index is a literal type

type index = "name"
type x = Employee[index]

This does not compile because we are attempting to use a value as an index

let index:string = "name"

type x = Employee[index]


KeyOf operator - index type query

The KeyOf type operator is an operator that allows the retrieval of all the indexes of a type as a union type.

Where the individual component of the union types is the literal types that represent the properties of that type.

To illustrate given our Employee type:

type Employee = {
name: string
employedOn: Date
age: string
role: {
  title: string
  salary: number
  isManagement: boolean
  }
}

We can use keyof Employee to return all its indexes as union type. Which will be:
"name" | "employedOn" | "age" | "role"

Note that the keyof operator can only be used in a type position. Meaning it is only considered valid when used in places where TypeScript expects a type. This can be seen in this screenshot:

T[K] - indexed access operators

The T[k] operator allows us to specify the type that an index operation will return. 

It should be noted that T and K carry no special meaning and it could very well be written as G[B]. In fact, the notation can be seen as a generic representation for all Index Types. Hence, T[K] basically represents the type that will be returned if any index T is used with any type K.

A use case example will probably explain better.

Let us say we want to write a function that, given an object value and a property, extracts the value found via the given property on the given object. This function should also be type-safe.

Such a function would be able to return values of different types. This is because the return type depends on the property given and an object could have properties of different types. In such a case, we can represent the return value as T[K].

The code below shows an implementation of such a function. Note that we are also using keyOf which ensures that only properties that are found in the object are considered valid.    

function getByIndex<T, K extends keyof T>(obj: T, index: K): T[K] {
  return obj[index];
}

Which can be used as follows:

let person = {
  name: "foo",
  age: 20
}

let nameVal: string = getByIndex(person, "name")
let ageVal: number = getByIndex(person, "age")

Showing that the result type could vary (in the case, it is either string or number) depending on the index value to be retrieved.

Note that this function is also type-safe in the sense that it won’t compile if you attempt to pass in an index value that does not correspond to a property on the object:

getByIndex(person, "role")

This will fail with the following error message:

Argument of type '"role"' is not assignable to parameter of type '"name" | "age"'

This post is part of a series. See Introduction to Advance Types in TypeScript for more posts in the series.

No comments: