Execute Program

Advanced TypeScript: Any and Unknown in Generic Constraints

Welcome to the Any and Unknown in Generic Constraints lesson!

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

  • Suppose that we want a generic object type containing an items array. We want to ensure that items is actually an array, so we use a generic constraint. Our first attempt is <A extends Array>. Unfortunately, that's a type error: Array requires a generic type argument, but we didn't give it one.

  • >
    type ItemsObject<A extends Array> = {
    items: A
    };

    const itemsObject: ItemsObject<Array<number>> = {items: [1, 2, 3]};
    itemsObject;
    Result:
    type error: Generic type 'Array<T>' requires 1 type argument(s).Pass Icon
  • We need to provide a type argument to Array, like <A extends Array<number>>. But the number there isn't right. We want to allow arrays of any type, like ItemsObject<Array<string>> or ItemsObject<Array<{name: string}>>.

  • We might try to use any as the type parameter, like <A extends Array<any>>. That seems to solve the problem: we can build a type that works with any array as its type parameter.

  • >
    type ItemsObject<A extends Array<any>> = {
    items: A
    };

    const itemsObject: ItemsObject<Array<number>> = {
    items: [1, 2, 3]
    };
    itemsObject;
    Result:
    {items: [1, 2, 3]}Pass Icon
  • Our code correctly type errors when items isn't an array.

  • >
    type ItemsObject<A extends Array<any>> = {
    items: A
    };

    const stringObject: ItemsObject<string> = {
    items: 'Important Data'
    };
    stringObject;
    Result:
    type error: Type 'string' does not satisfy the constraint 'any[]'.Pass Icon
  • Now we want to write a function that takes an ItemsObject argument. The function should work with any ItemsObject: ItemsObject<Array<number>>, ItemsObject<Array<string>>, etc. That means that the function must have the same extends constraint as ItemsObject itself.

  • >
    type ItemsObject<A extends Array<any>> = {
    items: A
    };

    function getItemCount<A extends Array<any>>(
    itemsObject: ItemsObject<A>
    ): number {
    return itemsObject.items.length;
    }
    getItemCount({items: ['a', 'b', 'c']});
    Result:
    3Pass Icon
  • That code compiles, but now the any has become very dangerous! When checking the function's types, itemsObject.items is an Array<any>, so all of its elements have the any type!

  • In the next example, note that getFirstNumber's type says that it returns a number. But at runtime, we get a string instead.

  • >
    type ItemsObject<A extends Array<any>> = {
    items: A
    };

    function getFirstNumber<T extends Array<any>>(
    itemsObject: ItemsObject<T>
    ): number {
    const firstElement: number = itemsObject.items[0];
    return firstElement;
    }

    getFirstNumber({items: ['a', 'b', 'c']});
    Result:
    'a'Pass Icon
  • There are a couple of ways to remove that dangerous any. First, we can use unknown instead. With that change, our buggy getFirstNumber function causes a type error.

  • >
    type ItemsObject<A extends Array<unknown>> = {
    items: A
    };

    function getFirstNumber<T extends Array<unknown>>(
    itemsObject: ItemsObject<T>
    ): number {
    const firstElement: number = itemsObject.items[0];
    return firstElement;
    }

    getFirstNumber({items: ['a', 'b', 'c']});
    Result:
    type error: Type 'unknown' is not assignable to type 'number'.Pass Icon
  • We prevented the bug, but now for the bigger question: how do we write a getFirstElement function that works, and has the correct return type, no matter what type is in the array? One solution is to take the T in Array<T> as our type parameter.

  • >
    type ItemsObject<T> = {
    items: Array<T>
    };

    function getFirstElement<T>(
    itemsObject: ItemsObject<T>
    ): T {
    const firstElement: T = itemsObject.items[0];
    return firstElement;
    }

    getFirstElement({items: ['d', 'e', 'f']});
    Result:
    'd'Pass Icon
  • In that example, we sidestepped the original problem by taking a different approach. The original code with extends Array<any> was more generic than it needed to be. It's sufficient to take a <T> type parameter, then specify Array<T> wherever it's needed.

  • This is an important general principle: often, type constraint problems happen when we try to make code more generic than it needs to be. Always look for a simpler solution before resorting to complex types.

  • Now for a related question. What if we constrain a generic with <T extends any>? That's identical to simply saying <T>! An unconstrained type parameter like <T> already means "T can be any type".

  • Here's a generic type with no constraint:

  • >
    type Container<T> = {
    item: T
    };

    const container: Container<number> = {item: 1};
    container;
    Result:
    {item: 1}Pass Icon
  • >
    type Container<T> = {
    item: T
    };

    const container: Container<number> = {item: 'one'};
    container;
    Result:
    type error: Type 'string' is not assignable to type 'number'.Pass Icon
  • Here's an equivalent type with <T extends any>.

  • >
    type Container<T extends any> = {
    item: T
    };

    const container: Container<number> = {item: 1};
    container;
    Result:
    {item: 1}Pass Icon
  • >
    type Container<T extends any> = {
    item: T
    };

    const container: Container<number> = {item: 'one'};
    container;
    Result:
    type error: Type 'string' is not assignable to type 'number'.Pass Icon
  • They're both safe, and they produce exactly the same error message. There's no reason to ever type <T extends any> or <T extends unknown> because they mean the same thing as <T>.

  • Here's a code problem:

    The wrapSet function below wraps a set in an object. If we have a variable someSet: Set<number>, then wrapSet(someSet) returns an object with the type {theSet: Set<number>}.

    Unfortunately, there's currently a type error because we didn't specify Set's type parameter. Change the function's type to avoid the type error.

    Make sure not to use any, which could lead to unsafe code. Even without using any, there are multiple ways to solve this. Some possible solutions add more type complexity, but one possible solution removes type complexity!

    function wrapSet<T>(
    theSet: Set<T>
    ): {theSet: Set<T>} {
    return {theSet};
    }
    const set1 = wrapSet(new Set<number>([1]));
    const set2 = wrapSet(new Set<string>(['a']));
    // We convert the sets into arrays so we can actually see what's inside them.
    [[...set1.theSet], [...set2.theSet]];
    Goal:
    [[1], ['a']]
    Yours:
    [[1], ['a']]Pass Icon
  • To summarize this lesson:

    • Using any as a placeholder in generic constraints, like <A extends Array<any>>, is sometimes unsafe.
    • It's safe to use unknown in constraints, like <A extends Array<unknown>>. But that means that the individual array elements will be unknowns. We can work with the array (like by accessing array.length) but we can't do anything with the unknown elements.
    • If we need to access the elements, we can use the element type itself as a type parameter like <T>, then type the array as Array<T>. It's a good idea to start with this solution. In most cases, it's all we need.