Transforming a Collection
A transformation takes a collection, such as an array or a Map, and builds a new collection from it. The original is not changed.
TypeScript arrays already carry most of these operations as methods: map, filter, flat, flatMap and join. You call them on the array and get a new value back, so no loop is needed. A few operations, like zip, have no built-in method — for those you write a small helper function once and reuse it.
A helper can be generic. A generic function works with any element type: you write function zip<A, B>(first: A[], second: B[]): [A, B][] once and use it for numbers, strings and objects alike. The type parameters A and B are filled in by the compiler at the call site.
Map
The map operation applies the same function to every element and collects the answers in a new array of the same length. For example, it can turn [ 1, 2, 3 ] into [ 3, 6, 9 ].
Arrays have a built-in map method. You pass it a function, and it returns the new array. The type of the result follows the function: if the function returns a string, you get a string[].
Do not confuse this with the Map class, which stores key and value pairs. Here "map" means "apply a function to every element".
Map every element
numbers.map(x => x * 3) calls the function on every element and returns a new array. The original numbers array is printed too, to show that it still holds [ 1, 2, 3 ].
Output: TypeScript file and captured output
const numbers = [1, 2, 3]
// map calls the function once for every element
const tripled = numbers.map(x => x * 3)
console.log(numbers)
console.log(tripled)
Map with the index
Sometimes the position of an element matters too. The map callback gets the index as its second argument, so you can use both. Here each value is multiplied by its own index, and the first element becomes 0.
Output: TypeScript file and captured output
const numbers = [1, 2, 3]
// the second argument of the callback is the position of the element
const result = numbers.map((value, idx) => value * idx)
console.log(result)
Skip some results
Sometimes some results should be dropped. Let the callback return undefined for those elements, then remove them with filter. The helper isDefined is a type guard: its return type value is T tells the compiler that every value that passes the filter is no longer undefined. That narrows the result type from (number | undefined)[] to number[].
Output: TypeScript file and captured output
const numbers = [1, 2, 3]
// the callback returns undefined for the elements we want to drop
const maybe = numbers.map(x => (x === 2 ? undefined : x * 3))
// a type guard tells the compiler that the kept values are never undefined
function isDefined<T>(value: T | undefined): value is T {
return value !== undefined
}
const result: number[] = maybe.filter(isDefined)
console.log(maybe)
console.log(result)
Transform the keys
To change the keys of a Map, spread it into an array of [key, value] pairs with [...numbersMap], map every pair to a new pair, and hand the result to new Map(...). Here toUpperCase() changes each key. A Map keeps its entries in insertion order, so the output is always in the same order.
Output: TypeScript file and captured output
const numbersMap = new Map([["key1", 1], ["key2", 2]])
// spread the Map into pairs, change every key, build a new Map
const upperKeys = new Map(
[...numbersMap].map(([key, value]): [string, number] => [key.toUpperCase(), value])
)
console.log(numbersMap)
console.log(upperKeys)
Transform the values
To change the values, do the same but keep the key and compute a new value. Here each value grows by the length of its key: 1 + "key1".length is 5.
Output: TypeScript file and captured output
const numbersMap = new Map([["key1", 1], ["key2", 2]])
// keep the key, compute a new value
const newValues = new Map(
[...numbersMap].map(([key, value]): [string, number] => [key, value + key.length])
)
console.log(newValues)
Zip
Zipping takes two arrays and pairs up their elements by position: the first with the first, the second with the second, and so on. The result has as many pairs as the shorter array.
There is no built-in zip method, so a small helper is the normal answer. A pair is written as a tuple type, [A, B]: an array of fixed length where each position has its own type. Find the shorter length with Math.min and loop with an index to build the pairs.
Zip two arrays
The generic zip helper returns a [string, string][] here: an array of two-element tuples. The loop runs to the shorter length and builds one tuple per index. Printing it shows each pair as its own small array.
Output: TypeScript file and captured output
// zip pairs up the elements by position and stops at the shorter array
function zip<A, B>(first: A[], second: B[]): [A, B][] {
const n = Math.min(first.length, second.length)
const pairs: [A, B][] = []
for (let i = 0; i < n; i++) {
pairs.push([first[i], second[i]])
}
return pairs
}
const colors = ["red", "brown", "grey"]
const animals = ["fox", "bear", "wolf"]
const pairs = zip(colors, animals)
console.log(pairs)
Zip with a transformation
You do not always need the pairs themselves. Here the loop uses both elements straight away to build a sentence. animals[i][0] is the first letter, made uppercase with toUpperCase(), and animals[i].slice(1) is the rest of the word. The sentence is built with a template string.
Output: TypeScript file and captured output
const colors = ["red", "brown", "grey"]
const animals = ["fox", "bear", "wolf"]
const n = Math.min(colors.length, animals.length)
for (let i = 0; i < n; i++) {
// uppercase only the first letter of the animal name
const name = animals[i][0].toUpperCase() + animals[i].slice(1)
console.log(`The ${name} is ${colors[i]}`)
}
Unzip an array of pairs
Unzipping is the reverse: one array of pairs becomes two arrays. The for...of loop destructures each tuple into word and value and pushes the parts onto two separate arrays.
Output: TypeScript file and captured output
const numberPairs: [string, number][] = [["one", 1], ["two", 2], ["three", 3]]
const words: string[] = []
const values: number[] = []
// destructuring gives both parts of the pair a name
for (const [word, value] of numberPairs) {
words.push(word)
values.push(value)
}
console.log(words)
console.log(values)
Associate
Associating builds a Map from an array. Each element gives you one key and one value. A Map cannot hold the same key twice, so if two elements produce the same key, the later one wins.
The short way is to map every element to a [key, value] tuple and pass that array to new Map(...). Write the return type of the callback as [string, number] so the compiler sees a tuple and not a plain array.
Elements as keys
The element itself becomes the key, and a value is computed from it. Here the words are keys and their lengths are values, so lengths.get("three") is 5.
Output: TypeScript file and captured output
const words = ["one", "two", "three"]
// each word becomes a key, its length becomes the value
const lengths = new Map(words.map((word): [string, number] => [word, word.length]))
console.log(lengths)
console.log(lengths.get("three"))
Elements as values
The element becomes the value, and the key is computed from it. Here the key is the uppercase first letter. Both two and three start with T, so three replaces two and the size of the Map is 2.
Output: TypeScript file and captured output
const words = ["one", "two", "three"]
// the key is the first letter; a later word replaces an earlier one
const byFirstLetter = new Map(
words.map((word): [string, string] => [word[0].toUpperCase(), word])
)
console.log(byFirstLetter)
console.log(byFirstLetter.size)
Compute both key and value
You can compute both the key and the value from the element. Here the key is the first letter and the value is the word length. Again the later T entry wins, so T holds 5, the length of three.
Output: TypeScript file and captured output
const words = ["one", "two", "three"]
// the key is the first letter, the value is the length of the word
const lengthByLetter = new Map(
words.map((word): [string, number] => [word[0].toUpperCase(), word.length])
)
console.log(lengthByLetter)
Key and value from one element
Sometimes one element carries both parts. split(" ") cuts the full name at the space into an array of words. The last name becomes the key and the first name becomes the value.
Output: TypeScript file and captured output
const names = ["Alice Adams", "Brian Brown"]
// one element carries both parts: last name -> first name
const byLastName = new Map(
names.map((fullName): [string, string] => {
const parts = fullName.split(" ")
return [parts[1], parts[0]]
})
)
console.log(byLastName)
Flatten
Flattening turns a nested collection, such as an array of arrays, into one plain array with all the elements in order.
Arrays have two methods for this. flat() flattens one level of an existing array of arrays. flatMap(f) does both steps at once: it maps every element to an array and then flattens the results, which is handy when the inner arrays come out of the elements themselves.
Flatten an array of arrays
numberSets.flat() copies the elements of each inner array into one new array. Two inner arrays of three numbers become one array of six.
Output: TypeScript file and captured output
const numberSets = [[1, 2, 3], [4, 5, 6]]
// flat() copies the elements of every inner array into one new array
const result = numberSets.flat()
console.log(result)
console.log(result.length)
Flatten values from objects
Each Container object holds an array in its values field. flatMap(c => c.values) reads that field for every container and joins all the arrays into one. The eight values from the three containers end up together, and they are printed joined with commas so they fit on one line.
Output: TypeScript file and captured output
interface Container {
values: string[]
}
const containers: Container[] = [
{ values: ["one", "two", "three"] },
{ values: ["four", "five"] },
{ values: ["six", "seven", "eight"] }
]
// flatMap maps every element to an array and flattens the results
const allValues = containers.flatMap(c => c.values)
console.log(allValues.length)
console.log(allValues.join(", "))
String Representation
Often you want to show a collection as readable text, for example with commas between the elements.
The join method takes a separator and returns one string. It turns each element into text for you, so numbers need no extra step. join has no options beyond the separator, so the extras — a prefix, a postfix, a limit, or a transformation — are done around it: with +, with slice, or with a map before the join.
Join with a separator
words.join(", ") puts ", " between the words and returns one string.
Output: TypeScript file and captured output
const words = ["one", "two", "three"]
// join puts the separator between the elements
console.log(words.join(", "))
Add a prefix and a postfix
A prefix and a postfix are just text around the joined result. You add them with +.
Output: TypeScript file and captured output
const words = ["one", "two", "three"]
// a prefix and a postfix are just text around the joined result
const text = "start: " + words.join(" | ") + ": end"
console.log(text)
Limit the number of elements
For a long array you may want to show only the first few elements. Take the first 10 with numbers.slice(0, 10) and join them — join turns the numbers into text on its own. Then add ", <...>" at the end to show that more elements were cut off.
Output: TypeScript file and captured output
const numbers: number[] = []
for (let i = 1; i <= 100; i++) {
numbers.push(i)
}
// slice takes the first 10 elements; join turns the numbers into text
const limit = 10
const shown = numbers.slice(0, limit).join(", ")
const text = numbers.length > limit ? shown + ", <...>" : shown
console.log(text)
Transform each element before joining
To change every element before joining, map the array first and call join on the result. Here each word becomes "Element: " plus the word in uppercase.
Output: TypeScript file and captured output
const words = ["one", "two", "three"]
// change every element first, then join the results
const parts = words.map(word => "Element: " + word.toUpperCase())
console.log(parts.join(", "))