44.5 TypeScript 5.1–5.5: Isolated Declarations, Inferred Type Predicates
Alright, let’s talk about a couple of features from the TypeScript 5.x era that are less about flashy new syntax and more about making your life as a developer significantly better. These are the kind of tools that, once you use them, you’ll wonder how you ever lived without. They fix real, tangible pain points.
The --isolatedDeclarations Flag for Speed
If you’re building a library, you know the drill: you run tsc to compile your precious .ts files into .js and .d.ts declaration files. The declaration file generation has always been a bit of a bottleneck because, to be absolutely type-safe, the TypeScript compiler has to do a full type-check of your code. It can’t just spit out types based on local syntax; it needs the whole program context to know if function foo(): Bar is actually returning a Bar.
This is correct, but it’s slow. And if you’re using a transpiler like esbuild or swc for blisteringly fast JavaScript emission (because they skip type checking), you’ve been stuck with a frustrating choice: either run tsc again just for the .d.ts files (negating the speed gain) or lose your types. A classic “pick two” scenario: fast build, correct types, sane mind.
Enter --isolatedDeclarations in TypeScript 5.4. This flag tells tsc, “Hey, when you generate declarations, assume that every export is safe to generate in isolation.” It’s a contract you make with the compiler. In return, it can parallelize the declaration emission like crazy, because it no longer needs to deeply analyze how types interact across files.
The Catch (Because there’s always a catch): To use this mode, your code has to be well-behaved. The compiler will yell at you if you try to do things that require full-program knowledge. The biggest one? const re-exports.
// utils.ts
export const myConstant = { key: "value" } as const;
// index.ts - This will cause an error under --isolatedDeclarations
export { myConstant } from './utils';
// The error:
// error TS5069: Option 'isolatedDeclarations' requires that re-exported declarations have an explicit type annotation.
Why? Because the type of myConstant is { readonly key: "value" }. To re-export it correctly, the compiler in index.ts would need to know the exact type from utils.ts. In isolated mode, it won’t go look. The fix is simple, if a bit verbose:
// index.ts - The fixed version
import { myConstant } from './utils';
export const myConstant: typeof myConstant = myConstant;
// or, give it an explicit type annotation at the source
Best Practice: Use --isolatedDeclarations in your library builds, especially if you’re using a fast transpiler pipeline. It turns declaration generation from a sequential chore into a parallelized blast. Just be prepared to add a few type annotations to keep the compiler happy. It’s a small price to pay for the speed boost.
Inferred Type Predicates for Less Boilerplate
Remember writing type predicate functions? You’d have to check typeof value === 'string' and then explicitly declare the function return type: value is string. It was repetitive and felt like we were doing the compiler’s job. “Yes, TypeScript, I just checked this. Why do I have to spell it out for you?”
TypeScript 5.4 said, “Fair point.” Now, if you write a function that returns a boolean and performs a straightforward type check, TypeScript will often infer that the function is a type predicate.
function isString(value: unknown) {
return typeof value === 'string'; // TS now INFERS `value is string`
}
function isAdmin(user: User) {
return user.role === 'admin'; // TS now INFERS `user is AdminUser`
// (if `AdminUser` is a type with `role: 'admin'`)
}
const data: unknown = "hello";
if (isString(data)) {
console.log(data.toUpperCase()); // Safe! 'data' is narrowed to string.
}
Why this is brilliant: It eliminates a whole class of boilerplate. You write the logic, and TypeScript connects the dots. It makes writing type guards feel natural instead of ceremonial.
The Pitfall (Tread carefully here): The inference isn’t magic. It’s based on heuristics, and it can be fooled. It works best with simple, direct equality checks (===, !==) or typeof/instanceof operations. The moment your logic gets more complex, you’ll need to fall back to the manual is annotation.
// This will NOT be inferred as a type predicate. Too complex.
function isStringOrNumber(value: unknown) {
return typeof value === 'string' || typeof value === 'number';
}
// You must still manually annotate:
// function isStringOrNumber(value: unknown): value is string | number { ... }
// This is also dangerous and won't work correctly:
function isBadPredicate(obj: any) {
return !!obj?.some?.deeply?.nested?.property; // Not a type check!
}
// This returns a boolean, but tells us nothing about the *type* of `obj`.
Best Practice: Embrace it for simple checks. Let the compiler handle the grunt work. For anything more complex, or if the inference doesn’t kick in (always hover to check!), just manually annotate the predicate. It’s a fantastic quality-of-life improvement that makes your codebase both safer and less cluttered. It’s one of those features that just makes sense.