Home > Software design >  Return a different type if array is empty in Typescript
Return a different type if array is empty in Typescript

Time:01-27

I want to return a different type in the strict mode if the argument is an empty array:

public static fromArray<T extends number[] | ({ length: 0 } & never[])>(
  array: T,
): T extends { length: 0 } ? undefined : ListNode {
  if (array.length === 0) {
    return undefined; // Error: TS2322: Type 'undefined' is not assignable to type 'T extends { length: 0; } ? undefined : ListNode'.
  }

  const start = new ListNode(array[0]);

  ...

  return start; // Error: TS2322: Type 'ListNode' is not assignable to type 'T extends { length: 0; } ? undefined : ListNode'.
}

The expected behavior:

const a = ListNode.fromArray([]); // typeof a is undefined
const b = ListNode.fromArray([1]); // type of b is ListNode

If I add some type casting, e.g.

  return start as T extends { length: 0 } ? undefined : ListNode;

then all works as expected. Is it possible to implement this without "hacks"?

CodePudding user response:

Consider this example:

class ListNode<T> {
    constructor(arg: T) { }
}

type IsLiteralNumber<N extends number> =
    (N extends number
        ? (number extends N
            ? false
            : true)
        : true)
{
    // false
    type Test1 = IsLiteralNumber<number>

    // false, because TS is unaware how long is any[] array
    type Test2 = IsLiteralNumber<any[]['length']>

    // true, it is clear that provided array has 3 elements
    type Test3 = IsLiteralNumber<[1, 2, 3]['length']>

    // true, 5 is a literal type
    type Test4 = IsLiteralNumber<5>
}

/**
 * Whole trick here is to check whether T[length]
 * property has literal number type (1,2,3,4) or no (number)
 */
type IsTuple<T> =
    /**
     * Check whether T is an array
     */
    (T extends Array<any> ?
        /**
         * Check whether T[length] has literal number type
         */
        IsLiteralNumber<T['length']>
        /**
         * If T is not array it is obvious that it should be false
         */
        : false)

{
    /**
     * false, because is it ibvious that type number[] does 
     * not have fixed length
     */
    type Test1 = IsTuple<number[]>

    // true, fixed length is 0
    type Test2 = IsTuple<[]>

    // true, fixed length is 3
    type Test3 = IsTuple<[1, 1, 1]>

}

/**
 * If argument is Tuple infer literal type from 
 * first element, otherwise return a union of all types of array elements 
 */
type ListNodeHead<Tuple extends any[]> =
    /**
     * Check whether TUple is actually tuple
     */
    IsTuple<Tuple> extends true
    /**
     * If yes - infer exact type of first element
     */
    ? Tuple extends [infer H, ...infer _]
    ? ListNode<H>
    : never
    /**
     * Otherwise return a union of all elements type
     * Added undefined because array might be empty and this
     * length might be known only in runtime
     */
    : ListNode<Tuple[number]> | undefined


class Foo {
    public static fromArray(array: []): undefined
    public static fromArray<Elem extends number, Tuple extends Elem[]>(array: [...Tuple]): ListNodeHead<Tuple>
    public static fromArray<Tuple extends number[]>(
        array: Tuple
    ): undefined | ListNode<number> {
        return array.length === 0 ? undefined : new ListNode(array[0])

    }
}


const result = Foo.fromArray([]) // undefined
const result2 = Foo.fromArray([1]) // ListNode<1>

const foo = (arg: number[]) => Foo.fromArray(arg) // ListNode<number> | undefined

Playground

It works for literal types and more general types (works inside higher order function foo)

IsTuple check whether array has fixed length or not.

Head - returns first element if list is a tuple or just a type of elements in the list if it is not a tuple.

TS does not support conditional types as a return type, this is why I have overloaded fromArray method.

CodePudding user response:

Using a return type of ListNode | undefined would be totally sufficient, there is no reason for a conditional type:

public static fromArray<T extends number[] | ({ length: 0 } & never[])>(
  array: T,
): undefined | ListNode { /* method implementation */ }

What was your intention to use a conditional type expression here in the first place? Since length's is a runtime property and there is no separate "empty array" type (there are good reasons why even purely functional languages still treat empty lists as lists), there is no way to distinguish an empty array from an array on the type level.

  •  Tags:  
  • Related