At present, there exists a design constraint or a missing functionality in TypeScript. Refer to microsoft/TypeScript#33912 for an in-depth discourse on this issue.
The TypeScript compiler faces challenges when dealing with conditional types that rely on unspecified generic type parameters. Such types remain opaque to the compiler, leading to ambiguity in verifying assignability of values.
Upon invoking toArray(), the type parameter T
is assigned a specific type, and subsequently, the return type
T extends null | undefined ? [] : ...
can be determined. However, within the method body of
toArray()
, the type parameter
T
lacks specification. As a result,
T extends null | undefined ? [] : ...
becomes an ambiguous type. Despite checking if
value
is
null
, the type parameter
T
remains unspecified, causing the compiler to flag errors at each
return
statement:
function toArray<T>(
value: T
): T extends null | undefined ? [] : T extends ReadonlyArray<any> ? T : [T] {
if (value == null) { return []; } // error!
if (Array.isArray(value)) { return value; } // error!
return [value]; // error!
}
Until microsoft/TypeScript#33912 is resolved, the most viable option for handling such generic conditional types is to personally ensure that the implementation meets the call signature requirements. A recommended approach involves using a type assertion at points where the compiler cannot validate assignability:
type ToArray<T> =
T extends null | undefined ? [] :
T extends ReadonlyArray<any> ? T :
[T];
function toArray<T>(
value: T
): ToArray<T> {
if (value == null) { return [] as ToArray<T>; }
if (Array.isArray(value)) { return value as ToArray<T>; }
return [value] as ToArray<T>;
}
No errors are flagged by adopting this method. It's crucial to ensure accuracy while performing operations; changing value == null
to value != null
would not produce any errors. The compiler trusts assertions like as ToArray<T>
, hence truthfulness is important.
While this approach works, it can be cumbersome. Creating a type alias such as ToArray<T>
helps avoid repetitive typing, preventing the need for resorting to as any
. In complex scenarios, it may be better to utilize single-call-signature overloads:
// call signature
function toArray<T>(
value: T
): T extends null | undefined ? [] : T extends ReadonlyArray<any> ? T : [T];
// implementation
function toArray(value: unknown) {
if (value == null) { return []; }
if (Array.isArray(value)) { return value; }
return [value];
}
Function overload implementations offer more leniency compared to standard functions. Thus, no errors surface with this setup. Callers interact only with the singular call signature, while the implementation possesses flexibility to return values of type unknown
. This approach, though as risky as type assertions (e.g., != null
won't trigger errors), enhances code cleanliness and enforces a clear distinction between caller-visible typing and internal implementation logic.
Playground link to code