We have all been there. Throwing any at function parameters and object keys because we couldn't be bothered to type it precisely. I did that for a recent project and it was a cop out.
The problem is that with any, we lose many of the benefits typescript offers us, in this case efficient code completion.
Today, I am gonna be discussing precise types and how they can make our code better.
Example
The best way to illustrate this is with an example.
Suppose, we have a utility function pluck.
The function of pluck is to pull out all the values for a single field in an object. The objects are members of an array.
Properly typing the pluck function can help in making sure that we only use existing keys
that exist in the object.
I am going to create 4 variants of the pluck utility function. It would range from untyped to typed.
The pluck function without any type(no-pun intended) is this:
function pluck(records, key) {
return records.map((record) => record[key])
}Here the function is untyped and while it works at runtime, it opens up a can of errors.
We aren't sure that the key we use actually exist, which means we might be trying to access a property not present. The return type is also an any[]. It is still valid TS code but without any benefits.
function pluck(records: any[], key: string): any[] {
return records.map((record) => record[key])
}Here in the second version, it is slightly better but the string type is still too broad.
Remember that this string could also not exists on the object we need to access. The any type is also problematic.
function function pluck<T>(records: T[], key: string): any[]pluck<function (type parameter) T in pluck<T>(records: T[], key: string): any[]T>(records: T[]records: function (type parameter) T in pluck<T>(records: T[], key: string): any[]T[], key: stringkey: string) {
return records: T[]records.Array<T>.map<any>(callbackfn: (value: T, index: number, array: T[]) => any, thisArg?: any): any[]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((record: Trecord) => record[key])}We make the function a generic so that it can infer the specific type of the array. The type checker complains.
We cannot use a string key to index type unknown.
The problem here still persists and this is because string is still broad. It is stringly typed. The return type is also still any[]
function function pluck<T>(records: T[], key: keyof T): T[keyof T][]pluck<function (type parameter) T in pluck<T>(records: T[], key: keyof T): T[keyof T][]T>(records: T[]records: function (type parameter) T in pluck<T>(records: T[], key: keyof T): T[keyof T][]T[], key: keyof Tkey: keyof function (type parameter) T in pluck<T>(records: T[], key: keyof T): T[keyof T][]T) {
return records: T[]records.Array<T>.map<T[keyof T]>(callbackfn: (value: T, index: number, array: T[]) => T[keyof T], thisArg?: any): T[keyof T][]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((record: Trecord) => record: Trecord[key: keyof Tkey])
}We are making some progress here. We constrain the key so that it is only an existing key on the object. Typescript infers the return type.
Hover the function above and the return type is T[keyof T][].
In this case, if there are four keys with values of type number and string, the compiler infers the returned array as a string or number array.
It can be better.
function function pluck<T, K extends keyof T>(records: T[], key: K): T[K][]pluck<function (type parameter) T in pluck<T, K extends keyof T>(records: T[], key: K): T[K][]T, function (type parameter) K in pluck<T, K extends keyof T>(records: T[], key: K): T[K][]K extends keyof function (type parameter) T in pluck<T, K extends keyof T>(records: T[], key: K): T[K][]T>(records: T[]records: function (type parameter) T in pluck<T, K extends keyof T>(records: T[], key: K): T[K][]T[], key: K extends keyof Tkey: function (type parameter) K in pluck<T, K extends keyof T>(records: T[], key: K): T[K][]K): function (type parameter) T in pluck<T, K extends keyof T>(records: T[], key: K): T[K][]T[function (type parameter) K in pluck<T, K extends keyof T>(records: T[], key: K): T[K][]K][] {
return records: T[]records.Array<T>.map<T[K]>(callbackfn: (value: T, index: number, array: T[]) => T[K], thisArg?: any): T[K][]Calls a defined callback function on each element of an array, and returns an array that contains the results.map((record: Trecord) => record: Trecord[key: K extends keyof Tkey])
}Perfecto. In this final function, we have a generic function with two parameters. T and K which denotes the object and a key on the object. The second parameter K is a subset of keyof T Using this variant, we get straightfoward autocomplete of the known properties in the object.
Here is an concrete example:
type type RecordingType = "studio" | "live"RecordingType = 'studio' | 'live'
type type RecordType = {
artist: string;
title: string;
releaseDate: Date;
recordingType: RecordingType;
}
RecordType = {
artist: stringartist: string
title: stringtitle: string
releaseDate: DatereleaseDate: Date
recordingType: RecordingTyperecordingType: type RecordingType = "studio" | "live"RecordingType
}
const const recordsArr: RecordType[]recordsArr: type RecordType = {
artist: string;
title: string;
releaseDate: Date;
recordingType: RecordingType;
}
RecordType[] = [
{
artist: stringartist: 'Ed Sheeran',
title: stringtitle: 'Perfect',
releaseDate: DatereleaseDate: new var Date: DateConstructor
new () => Date (+4 overloads)
Date(),
recordingType: RecordingTyperecordingType: 'studio',
},
{
artist: stringartist: 'zedd feat Jon Bellion',
title: stringtitle: 'Beautiful now',
releaseDate: DatereleaseDate: new var Date: DateConstructor
new () => Date (+4 overloads)
Date(),
recordingType: RecordingTyperecordingType: 'live',
},
{
artist: stringartist: 'Zinolessky',
title: stringtitle: 'Many things',
releaseDate: DatereleaseDate: new var Date: DateConstructor
new () => Date (+4 overloads)
Date(),
recordingType: RecordingTyperecordingType: 'studio',
},
]
const const artistArr: string[]artistArr = function pluck<RecordType, "title">(records: RecordType[], key: "title"): string[]pluck(const recordsArr: RecordType[]recordsArr, 'title')Conclusion
Typescript offers us a lot quality of life improvement for DX. Even though it can be tempting to throw any all over the place, using precise types benefits far outweighs the trouble of thinking a bit more about our types.
The effective typescript book is great. Check it out as well here