T extends U ? X : Y is a type-level if: it resolves to X when T is assignable to U, otherwise Y. While T or U still contains an unresolved type parameter the conditional is deferred: it stays as written until the alias is instantiated. That is why a function returning T extends string ? "s" : "n" cannot return a plain "s" from its body: the compiler cannot know which branch applies.
Distribution. If the checked type is a naked type parameter (just T, not T[], keyof T or [T]), the conditional is distributive. Instantiated with a union, it is applied to each member and the results are unioned:
IsString<string | number>isboolean, because it istrue | false.neveris the empty union, so a distributive conditional overneveryieldsnever, not either branch.anyevaluates to the union of both branches.
Distribution is usually what you want: Exclude<T, U> and Extract<T, U> rely on it to filter unions.
Turning it off. Wrap both sides in a tuple: [T] extends [U]. The union is now compared as one type. This is also the reliable way to detect never ([T] extends [never]).
Constraints in the true branch. Inside X, T is treated as also being U, and infer can name parts of the matched type (see the infer question).
Distribution depends only on the written form of the check type. It happens at instantiation, so a non-generic string | number extends string ? 1 : 2 is just 2, and the same check through a naked T gives a union.
Interview-level summary: conditional types are lazy, distribute over naked type parameters, collapse on never, and are opted out with [T] extends [U].