Conditional types in TypeScript are a powerful way to create flexible and type-safe code, but their true strength lies not in abstract examples, but in solving concrete, everyday problems.
Let’s see them in action with a common scenario: creating a utility type that extracts all readonly properties from an object type.
type ReadonlyProps<T> = {
[K in keyof T as T[K] extends Readonly<any> ? K : never]: T[K];
};
interface MyObject {
readonly prop1: string;
prop2: number;
readonly prop3: boolean[];
prop4: { readonly nested: string };
}
type ReadonlyObjectProps = ReadonlyProps<MyObject>;
// Expected: { readonly prop1: string; readonly prop3: boolean[]; }
Here, ReadonlyProps<T> iterates over each key K in the type T. The as clause is where the magic happens: it uses a conditional type T[K] extends Readonly<any> ? K : never to decide whether to keep the key K or remap it to never. If the type of the property T[K] is assignable to Readonly<any> (meaning it’s a readonly primitive, a readonly array, or a readonly object), the key K is preserved. Otherwise, it’s mapped to never, effectively filtering it out. Notice how prop4’s nested readonly property is not included, because ReadonlyProps only checks the top-level property type.
This pattern of filtering or transforming keys based on their types is incredibly useful. Consider a scenario where you want to create a type that represents only the mutable properties of an interface, or one that extracts all properties that are functions.
Let’s build a type that extracts only the function properties from an object:
type FunctionProps<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
interface MyService {
getData(): Promise<string>;
postData(data: any): void;
timeout: number;
config: {
retries: number;
};
}
type ServiceMethods = FunctionProps<MyService>;
// Expected: { getData(): Promise<string>; postData(data: any): void; }
Again, the as clause is doing the heavy lifting. T[K] extends Function ? K : never checks if the type of the property T[K] is assignable to Function. If it is, the key K is kept; otherwise, it’s mapped to never, effectively removing it from the resulting type. This is invaluable for generating types that represent only the callable parts of an API or service.
A more nuanced use case emerges when you need to conditionally change the type of a property, not just filter it. Imagine you have a configuration object where certain string values should be treated as file paths, and you want to ensure that type safety is maintained. You might want to transform all string properties into a specific FilePath type, but only if they meet certain criteria.
Consider this: you’re building a type for a configuration object that needs to handle both plain strings and a special URL type. You want to ensure that any string property that looks like a URL is typed as URL, while other strings remain as string.
type URL = string & { __brand: 'URL' };
function isURL(str: string): str is URL {
try {
new URL(str);
return true;
} catch {
return false;
}
}
type TransformURLs<T> = {
[K in keyof T]: T[K] extends string
? isURL(T[K] as string) // This is a runtime check, not a compile-time type check!
? URL
: T[K]
: T[K];
};
The above example highlights a common pitfall: attempting to use runtime checks (isURL) directly within a conditional type. TypeScript’s conditional types operate purely on types, not on values or runtime behavior. The T[K] extends string part is correct, but isURL(T[K] as string) will not work as intended at compile time.
The correct way to achieve this kind of transformation based on a type-level characteristic of a string (or any type) involves more advanced type-level programming, often leveraging template literal types. For instance, if you had a string literal type that was a valid URL, you could transform it.
A more practical, type-level approach for transforming string values based on their potential structure (not their runtime value) would look like this, using a hypothetical IsURLStringType type that could be defined with advanced string manipulation:
// Hypothetical type-level check for URL-like strings
type IsURLStringType<S extends string> = S extends `${infer Protocol}://${string}` ? true : false;
type ConfigWithURLs = {
apiEndpoint: string;
assetPath: string;
defaultUrl: "http://example.com";
};
type ProcessedConfig<T> = {
[K in keyof T]: T[K] extends string
? IsURLStringType<T[K]> extends true
? URL // Using the URL brand from before
: T[K]
: T[K];
};
type FinalConfig = ProcessedConfig<ConfigWithURLs>;
// If IsURLStringType worked perfectly for string literals,
// defaultUrl would be of type URL, while apiEndpoint and assetPath would be string.
This illustrates that the power of conditional types lies in manipulating the type system itself. The most surprising aspect is how often you can achieve complex type transformations simply by remapping keys based on simple extends checks, or by using the infer keyword within conditional types to deconstruct and reconstruct types.
The next frontier in leveraging conditional types often involves recursive types and advanced mapped types to handle deeply nested structures or to infer complex relationships between types.