Concept
The beginner framing: beyond combining existing types with unions and intersections, TypeScript can compute new types from existing ones, branching on a condition, transforming every property of an object type, or building string types out of other string types.
Conditional types: T extends U ? X : Y
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"A conditional type evaluates a type-level extends check and picks one of two branches, structurally identical in spirit to a ternary expression, just operating on types instead of values.
Distribution: the surprising default behavior over unions
type ExcludeString<T> = T extends string ? never : T;
type Result = ExcludeString<string | number | boolean>;
// Result = number | boolean, NOT evaluated once against the whole union!type ExcludeString<T> = T extends string ? never : T;type Result = ExcludeString<string | number | boolean>;
`ExcludeString<string | number | boolean>` doesn't evaluate the conditional ONCE against the whole union, a conditional type with a bare type parameter DISTRIBUTES, evaluating separately for each union member.
Confirmed by compiling this and stepping through the mechanism: when the checked type (T) is a bare type parameter and the input is a union, the conditional type distributes, it's evaluated separately for each union member, and the results are unioned back together. This is exactly how TypeScript's built-in Exclude<T, U> and Extract<T, U> are implemented, they aren't special primitives, just this distribution behavior applied directly.
Preventing distribution: wrap T in a tuple
type IsStringDistributed<T> = T extends string ? true : false;
type A = IsStringDistributed<string | number>; // true | false, DISTRIBUTES
type IsStringWhole<T> = [T] extends [string] ? true : false;
type B = IsStringWhole<string | number>; // false, evaluates the UNION AS A WHOLEConfirmed by compiling both against the identical string | number input: wrapping both sides of the extends check in a single-element tuple ([T] extends [string]) suppresses distribution, TypeScript now checks whether the entire string | number union, as one unit, is assignable to string (it isn't, since number isn't a string), producing a single false rather than distributing to true | false. This tuple-wrapping trick is a well-established, deliberate idiom specifically for the cases where distribution isn't the desired behavior.
Mapped types: transforming every property
type Readonly2<T> = { readonly [K in keyof T]: T[K] };
type Optional2<T> = { [K in keyof T]?: T[K] };
interface User { name: string; age: number }
type ReadonlyUser = Readonly2<User>; // { readonly name: string; readonly age: number }A mapped type iterates over a type's keys (keyof T) and produces a new property for each, optionally transforming the value type or adding modifiers (readonly, ?) along the way. This is the exact mechanism behind Partial/Readonly/Record, covered in Utility Types.
Key remapping: the as clause
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface User { name: string; age: number }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
const getters: UserGetters = {
getName: () => "Ada",
getAge: ()
Confirmed by compiling this exact type and a matching implementation: the as clause inside a mapped type lets you rename each generated property, not just transform its value, here, combined with a template literal type, name becomes getName and age becomes getAge, with the corresponding value type changed to a zero-argument function returning the original property's type.