after diving into source codes for a whole day, I think I got the answer:
In fact <T> was inferred as 'a' and 'b' from T and [T] separately, which were called candidates types. These two candidates types have different priority: T in union type definition has a special priority value 1 (View https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts, line 4398, the NakedTypeVariable enum value), while [T] has a common priority value 0, which means higher in priority orders.
Then the higher priority value 0 will overwrite 1 immediately, before comparing the type details. That's why the type 'a' was erased. If they share the same priority value, then they will be merged(see below).
The simplest way to fix is removing <T extends 'a' | 'b'>. without any assertion, T in [T] will be inferred as a wider type string after destructuring a virtual expression ['b'].
I also found how the compiler process when multiple candidate types existed: If candidate types share a basic (like string, number) covariant type, then join them with union symbol |. Otherwise will get the leftmost type for which no type to the right is a super type.
Twill be inferred from one parameter asaand will be checked against the other parameter. This will work understrictNullChecks:function f<T extends [SingleOrArray<'a' | 'b'>, SingleOrArray<'a' | 'b'>] >(...a: T) { return a[0] && a[1] } let d = f('a', ['b'])