Execute Program

TypeScript Basics: Generic Functions

Welcome to the Generic Functions lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We've seen that arrays are generic data types: they can hold any type of data. What if we want to write a function that works on any type of data?

  • We can define generic functions using the same <> syntax. When a function is generic, it has a "type hole", just like Array does. The hole must be filled by a type, like how number fills Array<number>.

  • In the example below, we name the type hole T. This is a common generic type parameter name, and it works well enough in simple situations.

  • >
    function first<T>(elements: Array<T>): T {
    return elements[0];
    }
    first<boolean>([true, false]);
    Result:
    truePass Icon
  • We fill the type hole when we call the function. For example, first<boolean> fills the type hole with boolean. Let's trace the boolean type's path through the function, imagining what the compiler does. At each step, it replaces the T type parameter with the actual type.

  • (The next few examples are pseudocode, meant to show how the boolean type fills the type holes. Unlike most code in this course, we won't actually run these examples.)

  • First, the boolean in our function call fills the type hole T in the first<T> function definition.

  • >
    function first<boolean>(elements: Array<T>): T {
    return elements[0];
    }
    first<boolean>([true, false]);
  • Second, the argument (elements: Array<T>) becomes (elements: Array<boolean>).

  • >
    function first<boolean>(elements: Array<boolean>): T {
    return elements[0];
    }
    first<boolean>([true, false]);
  • The array that we pass to first<boolean> must match the definition, which is now Array<boolean>. Fortunately, the array that we're passing, [true, false], does match that type!

  • Third, the return type of first is T, which is still boolean, so first<boolean> returns a boolean.

  • >
    function first<boolean>(elements: Array<boolean>): boolean {
    return elements[0];
    }
    first<boolean>([true, false]);
  • Fourth and finally, all of the types match, so the function type checks. Just for completeness, we'll delete the T type parameter from our pseudocode, both in the definition of first and in our function call, first([true, false]). That leaves us with a new version of first that only works on booleans.

  • >
    function first(elements: Array<boolean>): boolean {
    return elements[0];
    }
    first([true, false]);
  • Tracing generic types like this will feel awkward at first. But so did tracing a series of function calls when you first learned to do it! Like anything in programming, it gets easier with practice. And continuing through this course will give you a lot of that practice!

  • When we call a generic function, we fill the type hole using <>. These types must match! For example, if we call first<string>, then we have to pass an array of strings.

  • >
    function first<T>(elements: Array<T>): T {
    return elements[0];
    }
    first<string>(['a', 'b']);
    Result:
    'a'Pass Icon
  • >
    function first<T>(elements: Array<T>): T {
    return elements[0];
    }
    first<string>([1, 2]);
    Result:
    type error: Type 'number' is not assignable to type 'string'.Pass Icon
  • >
    function first<T>(elements: Array<T>): T {
    return elements[0];
    }
    first<string>('a');
    Result:
    type error: Argument of type 'string' is not assignable to parameter of type 'string[]'.Pass Icon
  • The type parameters can vary every time we use the function: first<string>, first<number>, etc. This is where generics get their name. Generic means "relating to a class or group of things; not specific." Our first function works with any array contents. It's not specific; it's generic.

  • Generics allow us to write one function that can be reused with many types. Dynamic languages like JavaScript can also do this. However, TypeScript generics let us do it while maintaining our guarantee that the types all match.

  • >
    function last<T>(elements: Array<T>): T {
    return elements[elements.length - 1];
    }
    let results: [string, number] = [
    last<string>(['a', 'b', 'c']),
    last<number>([1, 2, 3]),
    ];
    results;
    Result:
    ['c', 3]Pass Icon
  • Generics also work when we assign the result to an inferred variable.

  • >
    function first<T>(elements: Array<T>): T {
    return elements[0];
    }
    let n = first<number>([1, 2]);
    let n2 = n;
    n2;
    Result:
    1Pass Icon
  • In that example, n and n2 both have the inferred type number. We can trace the compiler's logic again:

    • The compiler sees us calling first<T> as first<number>.
    • So T is number.
    • So first<T>(elements: Array<T>): T returns a number.
    • That return value is assigned to n, so n must be a number.
    • Then n is assigned to n2, so n2 must be a number.
  • When you compile your source code, the compiler performs thousands of these small type checking steps. At the end, all of the types must match. If anything doesn't match, that's a type error.

  • >
    function second<T>(elements: Array<T>): T {
    return elements[1];
    }
    let n = second<number>([1, 2]);
    let n2: string = n;
    n2;
    Result:
    type error: Type 'number' is not assignable to type 'string'.Pass Icon
  • We named our type variable T, and you'll see a lot of real-world code and examples using that name. It just means "this is the function's type parameter". That's fine for small functions like first and second, but more complex functions deserve specific type names. For example, Array<Element> is more specific than Array<T>: it tells us that the type parameter is used as an array element type.

  • >
    function first<Element>(elements: Array<Element>): Element {
    return elements[0];
    }
    first<boolean>([true, false]);
    Result:
    truePass Icon
  • Our generic examples have all involved arrays so far, because we haven't seen enough TypeScript features to build other kinds of generic examples. However, there's nothing special about arrays!

  • For example, we can write a function that says "I take any value, and return an array containing that value." In that case, there's still an array involved in the return value. But the function's argument doesn't involve any arrays at all.

  • >
    function wrapInArray<T>(value: T): Array<T> {
    return [value];
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    wrapInArray(1);
    Result:
    [1]Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    wrapInArray('a');
    Result:
    ['a']Pass Icon
  • As we see more TypeScript features, we'll also see how they interact with generic types.