Type checking for overloaded function statements is a bit weird and not well documented. The set of call signatures is compared to the implementation signature, and if the compiler decides that they aren't sufficiently "related" to each other, then it will issue an error (albeit not a very helpful error, see ms/TS#48186).
In your example code, the compiler has to infer the implementation return type from the implementation body. For transform1 it infers string | 0 (which you can verify by copying the implementation to a new function and seeing the inferred return type), since value[0] is getting a character of a string, I think. For transform2 it infers T | 0 (again, verifiable by copying), since value is narrowed to T, and transform2([value]) returns T[], and indexing into it is T.
That means, from the type checker's standpoint, these are your functions:
function transform1<T extends string>(value: T): T;
function transform1<T extends string>(value: T[]): T[] // error!
function transform1<T extends string>(value: T | T[]): string | 0 {
throw new Error()
}
function transform2<T extends string>(value: T): T;
function transform2<T extends string>(value: T[]): T[];
function transform2<T extends string>(value: T | T[]): T | 0 {
throw new Error()
}
(And so recursion is a bit of a red herring here.)
The first one errors because string | 0 is not seen as properly "related" to T | T[], while the second one is acceptable because T | 0 is seen as properly "related" to T | T[]. I don't really know why; for the first one it seems obvious that T[] cannot possibly be a string or 0 since T extends string. For the second one it seems equally impossible that T[] cannot possibly be a string or 0 for the same reason. All that means is that the compiler isn't really using such logic to allow/disallow call signatures. I guess that the presence of the generic causes the compiler to be more lenient and not bother checking if T and T[] are compatible or not.
In looking for an authoritative answer I rediscovered a somewhat related issue at microsoft/TypeScript#44661, where, when asked about the rules for comparing overload call signatures with implementations, @RyanCavanaugh said:
The rules are a mess of backcompat that no one wanted to write down.
Basically the idea is to detect totally wrong situations and not too much else extra, keeping in mind the limitation that we're analyzing the function body opaquely.
So that's the closest I can get to an answer: string | 0 is "totally wrong" for T[], whereas T | 0 is, apparently, not "totally wrong", at least to the extent the compiler's cursory check for "wrongness" can detect.
Playground link to code
transform1the compiler infersstring | 0which it does not see as related toT | T[], but fortransform2it infersT | 0which it does see as related toT | T[]. The exact rules are weird, though and I don't know how well documented they are.