Learn why TypeScript throws TS18047 when a value might be null — useRef.current, DOM queries, database lookups — and how to fix it properly.
TS18047 means you used a value that TypeScript believes could be null at that point in the program. The compiler is not being pedantic: somewhere in that value's type there is an explicit null — a DOM query that found nothing, a ref React has not attached yet, a database row that does not exist — and nothing between that declaration and this line ruled it out.
The check belongs to strictNullChecks, which strict: true turns on. Without it, TypeScript folds null into every type, this error disappears, and so does the protection against TypeError: Cannot read properties of null in production.
TypeScript 4.9 introduced the named wording. Before that, every one of these was the anonymous "Object is possibly 'null'" (TS2531). Now, when the offending expression is an identifier or a short dotted chain, the compiler tells you which value it means:
// The general shape of the error:
const heading = document.getElementById("page-title")
heading.textContent = "Orders"
// ~~~~~ Error: 'heading' is possibly 'null'.
//
// TypeScript sees: heading: HTMLElement | null
// You wrote: heading.textContent = … — unsafe if no #page-title existsuseRef<HTMLInputElement>(null) types current as HTMLInputElement | null, because on the first render there is no element yet. Effects run after the DOM is attached, but the compiler cannot prove that from the types.
// ❌ Broken
export function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current.focus()
// ~~~~~~~~~~~~~~~~ Error: 'inputRef.current' is possibly 'null'.
}, [])
return <input ref={inputRef} type="search" />
}// ✅ Fixed — optional chaining: if the input is not mounted, do nothing
export function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} type="search" />
}Use a guard instead — if (!inputRef.current) return — when the rest of the effect depends on the element. And always pass the element type: a bare useRef(null) types current as plain null, so after a check it narrows to never and every property access fails.
getElementById, querySelector and friends return null when the selector matches nothing — including when a typo, a late render or a different page means the node simply is not there.
// ❌ Broken
function clearSearch() {
const searchInput = document.querySelector<HTMLInputElement>("#search")
searchInput.value = ""
// ~~~~~~~~~~~ Error: 'searchInput' is possibly 'null'.
}// ✅ Fixed — fail loudly, then use the element freely
function clearSearch() {
const searchInput = document.querySelector<HTMLInputElement>("#search")
if (!searchInput) {
throw new Error("#search is not in the DOM")
}
searchInput.value = ""
}The guard narrows searchInput for the whole rest of the function, so this is a one-line cost no matter how many properties you touch below.
null When MissingORMs are deliberately honest about misses: Prisma's findUnique returns User | null, localStorage.getItem returns string | null, and a cache read is T | null by design.
// ❌ Broken
async function accountEmail(userId: string) {
const user = await prisma.user.findUnique({ where: { id: userId } })
return user.email
// ~~~~ Error: 'user' is possibly 'null'.
}// ✅ Fixed — decide what a missing row means, in the code
async function accountEmail(userId: string) {
const user = await prisma.user.findUnique({ where: { id: userId } })
if (!user) {
throw new Error(`No user with id ${userId}`)
}
return user.email
}Prisma also ships findUniqueOrThrow, whose return type is plain User — when a missing row is genuinely an error, picking the throwing API removes the null from the type instead of forcing a check at every call site.
String.prototype.match returns null when the pattern does not match. So does RegExp.prototype.exec. Indexing straight into the result is the classic version of this bug.
// ❌ Broken
function bearerToken(authHeader: string) {
const match = authHeader.match(/^Bearer (.+)$/)
return match[1]
// ~~~~~ Error: 'match' is possibly 'null'.
}// ✅ Fixed — optional chaining into the group, then one check
function bearerToken(authHeader: string): string {
const token = authHeader.match(/^Bearer (.+)$/)?.[1]
if (!token) {
throw new Error("Authorization header is missing a bearer token")
}
return token
}This shape generalises to anything that reports failure with null: URLSearchParams.get, JSON reviver results, document.querySelector inside a loop.
Read which value the message names. TS18047 quotes the exact expression — 'inputRef.current', not "the object". Hover it in your editor: the union containing null tells you whether the fix belongs on this line or in the declaration upstream.
Guard and exit early when null means something went wrong. if (!user) throw new Error(...) or an early return narrows the value for the rest of the function and documents the invariant. This is the right fix for required DOM nodes, required config, and rows that must exist.
Use ?. and ?? when null is a normal outcome. A ref that is not mounted yet, an avatar that was never uploaded, a cache miss — inputRef.current?.focus() and localStorage.getItem("theme") ?? "light" say "do nothing / use the default" far more clearly than a guard does.
Prefer APIs that throw over APIs that return null. findUniqueOrThrow instead of findUnique, or a small getElementOrThrow(selector) helper that returns a non-nullable element. Removing null from the type once beats checking for it at twenty call sites.
Copy the value into a const before narrowing. Narrowing a mutable property does not survive into a callback, because the compiler cannot prove nothing reassigned it in between:
// ❌ 'cart.discount' is possibly 'null' inside the callback
if (cart.discount) {
return amounts.map((amount) => amount * (1 - cart.discount.percent / 100))
}
// ✅ A local const cannot be reassigned, so the narrowing holds
const discount = cart.discount
if (discount) {
return amounts.map((amount) => amount * (1 - discount.percent / 100))
}Don't reach for ! or strictNullChecks: false. The non-null assertion compiles to nothing, so user!.email just moves the failure to runtime. Keep it for the narrow case where you hold a proof the compiler cannot see — an element you rendered yourself in the same component, checked one line above. Turning off strictNullChecks hides every error in this family and none of the crashes.
TS18047 fires under strictNullChecks when you use a value whose type includes null and TypeScript cannot prove it is non-null at that point. The usual sources are:
useRef<HTMLInputElement>(null) makes current an HTMLInputElement | nullgetElementById, querySelector, closest, event.targetfindUnique, localStorage.getItem, cache lookupsString.match and RegExp.exec, which return null when nothing matchesIf code that clearly could be null is not reporting the error, check that strict (or strictNullChecks) is actually on in the tsconfig.json covering that file.
Decide what should happen when the element is not mounted. If the answer is "nothing", optional chaining is the whole fix:
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])If the rest of the block depends on the element, guard once at the top with if (!inputRef.current) return. Two things to avoid: useRef(null) without a type argument, which types current as null and narrows to never after a check, and inputRef.current!.focus(), which is the same unchecked access you started with, minus the warning.
They are the same strictNullChecks check with two different messages. Up to TypeScript 4.8 every possibly-null access reported TS2531, "Object is possibly 'null'", which left you hunting through a long expression for the value it meant. TypeScript 4.9 added the named variants: when the expression is an identifier or a short property chain, you get TS18047 with the name printed.
const heading = document.getElementById("page-title")
heading.textContent = "Orders" // TS18047: 'heading' is possibly 'null'.
document.getElementById("page-title").textContent = "Orders" // TS2531: Object is possibly 'null'.The fixes are identical, so older Stack Overflow answers about "Object is possibly 'null'" apply directly. TS18048 is the same message for undefined, and TS18049 covers a value that could be either.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges