Reputation: 23
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]); // typeof 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"?
Upvotes: 2
Views: 1118
Reputation: 33111
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
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.
Q & A
- Could you elaborate on that part
N extends number? (number extends N in particular number extends N
?
N extends number
- means that N
is a subtype of number
type. N
has all props from number
and may have some other extra props.
See example:
type IsSubType<N> = N extends number ? true : false
type Test1 = IsSubType<number & { _tag: 'A' }> // true
type Test2 = IsSubType<5> // true
type Test3 = IsSubType<typeof Infinity> // true
All these types number & { _tag: 'A' }, 5, typeof Infinity
are subtypes of number
.
As for the second part: number extends N
. See this example:
/**
* This conditional type is important only in context of my answer,
*/
type IsSuperType<N> = number extends N ? false : true
type Test1 = IsSuperType<10> // true
type Test2 = IsSuperType<5> // true
type Test3 = IsSuperType<number> // false
Above IsSuperType
utility type checks whether number
is a subtype of N
. If it is a subtype - than N
is wrong because it might be much wider that number
. For instance:
type Test1 = IsSuperType<any> // true
So we need to make sure that number
is not subtype of N
.
See another example:
type IsLiteralNumber<N extends number> =
/**
* If N is a subtype of number
*/
(N extends number
/**
* and number is not subtype of N
*/
? (number extends N
? false
/**
* it is literal type of number: 5, 10, 42 ....
*/
: true)
: true)
- About because TS is unaware how long is any[] array ....
Here is a type of Array
from standard lib:
interface Array<T> {
/**
* Gets or sets the length of the array. This is a number one higher than the highest index in the array.
*/
length: number;
}
Property length
byt the default is just a number
. However, TS is able to infer the length of the literal array tuple
. That's it.
- Why then conditionals are allowed syntactically in the return type section if they are not supported
Conditional types are supported inside overload signatures because they (overloads) are less strict, in fact, they are bivariant.
Upvotes: 3
Reputation: 31
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.
Upvotes: 0