TypeScript’s Control Flow Analysis is generally effective at narrowing types. For instance, starting from TypeScript 4.4, type guards can influence control flow more effectively. Consider the following example:
// In TS 4.3 and below
function foo(arg: unknown) {
const argIsString = typeof arg === "string";
if (argIsString) {
console.log(arg.toUpperCase());
// ~~~~~~~~~~~
// Error! Property 'toUpperCase' does not exist on type 'unknown'.
}
}
In earlier versions of TypeScript, even though argIsString
is assigned the result of a type guard (typeof arg === "string"
), TypeScript would lose that information, resulting in an error. In TypeScript 4.4 and above, the control flow is improved, and this issue is resolved:
// In TS 4.4 and above
function foo(arg: unknown) {
const argIsString = typeof arg === "string";
if (argIsString) {
console.log(arg.toUpperCase()); // No error, TypeScript knows arg is a string now.
}
}
However, there are still situations where TypeScript’s type inference falls short. For example, when filtering an array that contains undefined
, null
, or other falsy values, TypeScript can’t determine whether the filtered array is completely truthy.
const fruits = ['apple', undefined, 'banana', undefined, 'cherry']; // (string | undefined)[]
const filteredFruits = fruits.filter(item => !!item); // (string | undefined)[]
Even though the undefined
values have been filtered out, TypeScript still thinks the result is of type (string | undefined)[]
, instead of string[]
. This can lead to type issues down the line, requiring manual type assertions or dealing with incorrect type assumptions.
To resolve this, you can use a custom type guard with TypeScript’s is
operator, which explicitly informs TypeScript of the correct type. Here’s how to create an isTruthyPredicate
function:
export function isTruthyPredicate<SomeType>(
item: SomeType | boolean | null | undefined,
): item is SomeType {
return !!item;
}
This predicate ensures that TypeScript understands when an item is truthy. By using this function in a filter operation, you can ensure TypeScript infers the correct type:
const filteredFruitsOnlyTruthy = fruits.filter(isTruthyPredicate); // string[]
Now, filteredFruitsOnlyTruthy
is correctly inferred as string[]
, eliminating the need for type assertions and making the code safer.
When searching, I found this answer is sensible too.
https://stackoverflow.com/q/51124837/3375906
In TypeScript, a boolean
is just a data type that can be either true
or false
, whereas the is
operator is specifically used for type-testing. It tells TypeScript that a particular condition not only checks a value but also narrows down its type. Let’s take a look at an example to understand the difference:
type Species = 'cat' | 'dog';
interface Pet {
species: Species;
}
class Cat implements Pet {
public species: Species = 'cat';
public meow(): void {
console.log('Meow');
}
}
function petIsCat(pet: Pet): pet is Cat {
return pet.species === 'cat';
}
function petIsCatBoolean(pet: Pet): boolean {
return pet.species === 'cat';
}
const p: Pet = new Cat();
p.meow(); // ERROR: Property 'meow' does not exist on type 'Pet'.
if (petIsCat(p)) {
p.meow(); // Now the compiler knows for sure that 'p' is of type Cat, and it has a 'meow' method
}
if (petIsCatBoolean(p)) {
p.meow(); // ERROR: Property 'meow' does not exist on type 'Pet'.
}
In this example:
petIsCat
is a custom type guard using the is
operator. It tells TypeScript that if the condition is true, the pet
is of type Cat
, and thus, the meow
method is available.petIsCatBoolean
only returns a boolean, and while it performs the same check, it doesn’t narrow the type. As a result, TypeScript doesn’t know that p
is a Cat
and throws an error when trying to access meow
.You can see the full example in the TypeScript playground here.
While TypeScript’s Control Flow Analysis has improved over the years, as seen in features introduced in TypeScript 4.4, there are still cases, like filtering out falsy values, where the type inference doesn’t work as expected. In such situations, custom type predicates using the is
operator can be a clean and effective solution to ensure TypeScript narrows types properly, improving both safety and developer experience.