Truthy and Falsy in TypeScript
if (value) is probably the condition you write most often, and it is also the one that quietly breaks the most things. A value is truthy when JavaScript coerces it to true in a boolean context. TypeScript inherits that behaviour unchanged, then adds something useful on top: it reads your truthy check and narrows the type inside the block.
In this article we will cover the complete falsy list, how truthiness narrows a union, the 0 and "" trap that produces real bugs in real apps, the difference between Boolean(x), !!x and if (x), and why .filter(Boolean) does not clean up your types the way you expect.
The falsy list is short, truthy is everything else
There is no list of truthy values worth memorising, because the list is "everything". There is a list of falsy values, and it has exactly eight entries:
| Value | Why it is falsy |
|---|---|
false | the boolean itself |
0 | zero |
-0 | negative zero, a distinct value |
0n | the BigInt zero |
'' | the empty string |
null | explicit absence |
undefined | implicit absence |
NaN | the result of a failed number parse |
Everything that is not on that list is truthy. That includes several values people expect to be falsy:
// Falsy: these are the only ones
const falsyList = [false, 0, -0, BigInt(0), '', null, undefined, NaN]
// BigInt(0) is the `0n` literal — spelled out so this compiles below an ES2020 target
// Truthy: everything else, including all of these
const surprisinglyTruthy = [[], {}, '0', 'false', ' ', -1, Infinity]An empty array is truthy. An empty object is truthy. The string '0' is truthy, and so is the string 'false'. A single space is truthy. If you have ever written if (userInput) to check "did the user type something", a space passed that check.
Truthy in TypeScript is still JavaScript
TypeScript does not change runtime behaviour here. Types are erased during compilation, so if (value) in your .ts file is the same if (value) in the emitted JavaScript, with the same coercion rules. This is one of the places where the line between the two languages is worth keeping clear — see TypeScript vs JavaScript if you want the fuller picture.
What TypeScript adds is compile-time understanding. The compiler knows which types can produce a falsy value and which cannot, and it uses your condition to refine the type for the rest of the block. That is control-flow analysis, and truthiness is one of the narrowing mechanisms it understands — alongside the typeof checks covered in TypeScript typeof.
How a truthy check narrows a type
This is the payoff. Write a plain condition and the type inside the branch gets smaller:
function formatDisplayName(name: string | null) {
if (name) {
// name is string here — .toUpperCase() is safe
return name.toUpperCase()
}
// name is null here
return 'Anonymous'
}No cast, no non-null assertion, no helper. The compiler follows the same logic you do: null is falsy, so inside the if the only remaining possibility is string.
The same works for the other direction with an early return, which is usually the nicer shape:
function describeSession(token: string | undefined) {
if (!token) {
return 'signed out'
}
// token is string from here to the end of the function
return `signed in with ${token.slice(0, 6)}`
}This is the everyday tool for working with union types that include null or undefined, and it is why optional properties feel so natural to consume — the | undefined they add is removed by a single truthy check.
The 0 and empty-string trap
Narrowing is the upside. Here is the downside, and it is the reason truthy checks earn their reputation:
function renderBadge(unread: number | undefined) {
if (unread) {
return `${unread} unread`
}
return 'All caught up'
}Call it with 0 and you get 'All caught up', which happens to be correct. Now change the meaning of the number:
function renderTemperature(celsius: number | undefined) {
if (celsius) {
return `${celsius}°C`
}
return 'No reading'
}Zero degrees is a perfectly good reading, and this function reports it as a missing sensor. The check conflated "absent" with "zero", because both are falsy. Empty strings have the identical problem: a user whose display name is '' is not a user without a name.
The fix is to check for the thing you actually mean:
function renderTemperatureFixed(celsius: number | undefined) {
if (celsius !== undefined) {
return `${celsius}°C`
}
return 'No reading'
}celsius !== undefined narrows exactly as well as if (celsius) does — you lose nothing in type safety and you stop lying about zero. The rule of thumb: a truthy check is fine when every falsy value in the type is genuinely "nothing". The moment 0, '' or false is a legitimate value, be explicit.
|| falls into the same trap, ?? does not
The logical OR operator uses truthiness, so default values have the exact same bug:
function resolvePageSize(configured: number | undefined) {
const viaOr = configured || 20 // 0 becomes 20
const viaNullish = configured ?? 20 // 0 stays 0
return { viaOr, viaNullish }
}|| replaces any falsy value. ?? replaces only null and undefined. For defaults, ?? is almost always the one you want — the nullish coalescing operator exists precisely because || was overloaded with truthiness for years.
The same split applies to assignment: ||= is truthy-based, ??= is nullish-based.
The falsy value that renders itself in JSX
If you write React, this is the truthiness bug you will actually ship:
// Renders a literal "0" on the page when count is 0
<div>{count && <Badge value={count} />}</div>&& returns the left operand when it is falsy, and React renders 0 as text. It skips false, null and undefined silently, but a numeric zero shows up in the DOM. The same code with an empty-string guard renders nothing, which is why the bug survives review — it only appears for one specific falsy value.
Make the condition a real boolean and the problem disappears:
[object Object]count > 0, Boolean(count) or a ternary all work. The underlying lesson is the same as everywhere else in this article: when 0 is a value rather than an absence, stop leaning on truthiness.
Boolean(x), !!x and if (x)
Three spellings, one coercion:
const rawHandle: string | null = 'ada'
const viaConstructor = Boolean(rawHandle)
const viaDoubleBang = !!rawHandle
const viaCondition = rawHandle ? true : falseAll three produce the same boolean. Boolean(x) is the most readable, !!x is the most common in compact expressions, and if (x) is implicit coercion at the statement level. There is no behavioural difference and no performance difference worth thinking about — pick whichever reads better in context.
One detail worth knowing: Boolean as a value is a function of type BooleanConstructor, which matters in the next section.
Why .filter(Boolean) does not clean up your types
This is the most-reported truthiness papercut in TypeScript. You have an array with holes in it, you filter them out, and the type does not improve:
const maybeHandles: (string | null)[] = ['ada', null, 'grace']
// Runtime: ['ada', 'grace']. Type: still (string | null)[]
const stillNullable = maybeHandles.filter(Boolean)At runtime the nulls are gone. At compile time the type is unchanged, so every element still looks like it might be null and the compiler keeps nagging. The reason is that filter's narrowing overload requires a type predicate — a signature of the form value is T — and Boolean's signature just returns boolean.
The fix is to give the compiler a predicate. Inline is fine for one-off cases:
const cleanHandles = maybeHandles.filter(
(handle): handle is string => handle !== null
)Or write it once as a reusable guard, which is what most codebases end up doing:
function isPresent<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
const presentHandles = maybeHandles.filter(isPresent)presentHandles is string[]. Note that isPresent checks for null and undefined specifically rather than truthiness, so it keeps 0 and '' — the same discipline as the section above, now applied to arrays.
If you want to see what this filtering looks like at the type level rather than the value level, the Filter challenge has you implement it on tuples, and Exclude is the same idea for unions.
Objects and arrays are always truthy
Worth repeating because it causes a specific class of bug:
const emptyBag: Record<string, number> = {}
const bagLooksTruthy = Boolean(emptyBag) // true
const bagActuallyHasEntries = Object.keys(emptyBag).length > 0 // false
const emptyList: string[] = []
const listLooksTruthy = Boolean(emptyList) // true
const listActuallyHasItems = emptyList.length > 0 // falseif (results) tells you a value was returned. It does not tell you the value contains anything. For collections, check .length or Object.keys(...).length; for an API response, check the field you actually care about.
strictNullChecks decides how much of this matters
With strictNullChecks off, null and undefined are assignable to every type, so string already secretly includes them and narrowing cannot help you — a truthy check is just a runtime habit with no compile-time payoff.
With it on (it is included in strict, and you should have it on), string | null is a real union that the compiler forces you to handle, and every truthy check becomes a narrowing step the type system tracks. Everything in this article assumes it is enabled. If you are not sure what your project does, that setting lives in tsconfig.json.
Quick reference
- Eight falsy values:
false,0,-0,0n,'',null,undefined,NaN. Everything else is truthy. [],{},'0','false'and' 'are all truthy.- A truthy check narrows the type inside the block — that part is free and worth using.
- Use it only when every falsy value in the type means "nothing". Otherwise compare explicitly.
- Prefer
??over||for defaults, and??=over||=. .filter(Boolean)does not narrow. Use a type predicate or a sharedisPresentguard.
The takeaway
Truthiness is a JavaScript coercion rule that TypeScript turned into a type-system feature. The narrowing it gives you is genuinely good — if (name) turning string | null into string with zero ceremony is one of the nicest things about writing TypeScript day to day.
The discipline is knowing when the shortcut lies. Any time the value could legitimately be 0, '' or false, a truthy check throws away a real case and the compiler will not warn you, because the code is perfectly valid. Write !== undefined, reach for ??, and keep if (value) for the cases where falsy really does mean missing.
Become a TypeScript Pro
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges