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
itemsarray. We want to ensure thatitemsis actually an array, so we use a generic constraint. Our first attempt is<A extends Array>. Unfortunately, that's a type error:Arrayrequires 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).
We need to provide a type argument to
Array, like<A extends Array<number>>. But thenumberthere isn't right. We want to allow arrays of any type, likeItemsObject<Array<string>>orItemsObject<Array<{name: string}>>.We might try to use
anyas 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]}Our code correctly type errors when
itemsisn'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[]'.
Now we want to write a function that takes an
ItemsObjectargument. The function should work with anyItemsObject:ItemsObject<Array<number>>,ItemsObject<Array<string>>, etc. That means that the function must have the sameextendsconstraint asItemsObjectitself.>
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:
3
That code compiles, but now the
anyhas become very dangerous! When checking the function's types,itemsObject.itemsis anArray<any>, so all of its elements have theanytype!In the next example, note that
getFirstNumber's type says that it returns anumber. But at runtime, we get astringinstead.>
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'
There are a couple of ways to remove that dangerous
any. First, we can useunknowninstead. With that change, our buggygetFirstNumberfunction 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'.
We prevented the bug, but now for the bigger question: how do we write a
getFirstElementfunction that works, and has the correct return type, no matter what type is in the array? One solution is to take theTinArray<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'
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 specifyArray<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}>
type Container<T> = {item: T};const container: Container<number> = {item: 'one'};container;Result:
type error: Type 'string' is not assignable to type 'number'.
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}>
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'.
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
wrapSetfunction below wraps a set in an object. If we have a variablesomeSet: Set<number>, thenwrapSet(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 usingany, 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']]
To summarize this lesson:
- Using
anyas a placeholder in generic constraints, like<A extends Array<any>>, is sometimes unsafe. - It's safe to use
unknownin constraints, like<A extends Array<unknown>>. But that means that the individual array elements will beunknowns. We can work with the array (like by accessingarray.length) but we can't do anything with theunknownelements. - If we need to access the elements, we can use the element type itself as a type parameter like
<T>, then type the array asArray<T>. It's a good idea to start with this solution. In most cases, it's all we need.
- Using