TypeScript Optional

September 26, 202611 min read
Requirements:
ObjectsFunctions| Unions

The word "optional" in TypeScript optional code covers four different features that all share one character: ?. Optional properties, optional parameters, optional chaining, and optional tuple elements. They look related, and they are, but each one has its own rules and its own way of surprising you.

This page walks through all of them, in the order you usually meet them.

What TypeScript optional properties really mean

A ? after a property name says the key may be missing.

interface UserProfile {
  id: number
  displayName: string
  nickname?: string
}
 
const minimalProfile: UserProfile = { id: 1, displayName: 'Ada' }
const fullProfile: UserProfile = { id: 2, displayName: 'Grace', nickname: 'Amazing Grace' }

Both assignments are valid. That is the write side of the story.

The read side is where people get tripped up. When you read nickname, its type is not string. It is string | undefined, because the key might not be there.

function announce(profile: UserProfile): string {
  const label = profile.nickname
  if (label === undefined) {
    return profile.displayName
  }
  return label
}

You cannot skip that check. TypeScript will not let a possibly-missing value flow into a slot that wants a string.

Optional is not the same as | undefined

It is tempting to treat nickname?: string and nickname: string | undefined as the same thing. They are not. The ? makes the key optional. The union only makes the value nullable — the key is still required.

interface ExplicitUndefined {
  nickname: string | undefined
}
 
// const missingKey: ExplicitUndefined = {}
// ❌ Property 'nickname' is missing in type '{}'
 
const explicitKey: ExplicitUndefined = { nickname: undefined } // ✅

So ? is the looser of the two. Use it when the key genuinely may not be present. Use | undefined when callers must make a deliberate decision and say so.

exactOptionalPropertyTypes

By default, ? also lets you write undefined into the key explicitly. That is usually harmless, but it blurs the line between "this field was never set" and "this field was cleared" — which matters a lot for PATCH-style payloads.

interface PatchPayload {
  nickname?: string
}
 
const clearIt: PatchPayload = { nickname: undefined }
// ✅ by default
// ❌ with "exactOptionalPropertyTypes": true in tsconfig.json —
//    Type 'undefined' is not assignable to type 'string'

Turn the flag on and ? means only "the key may be absent". If you want to allow an explicit undefined after that, you have to spell it out as nickname?: string | undefined. It is a strict flag worth enabling on new projects and worth being careful with on old ones.

Optional function parameters

The same ? marks a parameter as skippable at the call site.

function formatPrice(amount: number, currency?: string): string {
  if (currency === undefined) {
    return `${amount}`
  }
  return `${amount} ${currency}`
}
 
formatPrice(10)
formatPrice(10, 'EUR')

Inside the function, currency is string | undefined, exactly like an optional property. Same rule, same required check.

One structural constraint: an optional parameter cannot be followed by a required one.

// function badOrder(currency?: string, amount: number) {}
// ❌ A required parameter cannot follow an optional parameter

That is a positional-argument limitation, not a type-system one. If you find yourself fighting it, the answer is almost always a single options object instead of five positional parameters.

Optional versus a default value

A default value also makes the parameter skippable, but it changes the type inside the function body.

function formatWithDefault(amount: number, currency = 'EUR'): string {
  return `${amount} ${currency}`
}

Callers can still write formatWithDefault(10). But inside, currency is string, not string | undefined — the default already filled the hole. If you have a sensible fallback, reach for a default. It removes a check from every branch below it.

Optional methods

Interfaces can mark whole methods optional, in either of the two call-signature styles.

interface BuildPlugin {
  name: string
  setup?(): void
  teardown?: () => void
}
 
declare const cachePlugin: BuildPlugin
 
cachePlugin.setup?.()
cachePlugin.teardown?.()

Both forms mean the same thing to a caller: the method may not be there, so you cannot call it unguarded. This is the shape almost every plugin and lifecycle-hook API lands on, and ?.() is what makes consuming it bearable.

Optional properties in destructuring

Destructuring an optional property gives you the union, same as any other read. A default in the pattern closes it back up.

interface ConnectOptions {
  host: string
  port: number
  secure?: boolean
}
 
function openSocket({ host, port, secure = false }: ConnectOptions): string {
  return `${host}:${port} secure=${secure}`
}

Inside openSocket, secure is boolean — the default already handled the missing case. Drop the = false and it becomes boolean | undefined, and every use below needs a check. Defaults in destructuring patterns fire on undefined only, exactly like ??, so a stored false still comes through as false.

Optional chaining with ?.

Optional chaining reads a value that may not exist without blowing up at runtime.

interface Company {
  address?: {
    city: string
    zip?: string
  }
  employees?: string[]
  onHire?: (name: string) => void
}
 
declare const acme: Company
 
const cityName = acme.address?.city // string | undefined
const firstHire = acme.employees?.[0] // string | undefined
acme.onHire?.('Linus')

Three forms, one operator. ?. for properties, ?.[] for index access, and ?.() for calls. The call form is the one people forget, and it is the cleanest way to invoke an optional callback.

The important behaviour is short-circuiting. If any link in the chain is null or undefined, the whole expression stops there and evaluates to undefined. Nothing further down the chain runs.

declare const globex: Company
 
const zipCode = globex.address?.zip?.trim()

If address is missing, zip is never read and .trim() is never called. zipCode is string | undefined.

?? is not ||

Optional values pair naturally with the nullish coalescing operator. Reaching for || instead is one of the most common bugs in this whole area.

interface DisplayOptions {
  retries?: number
  title?: string
}
 
declare const opts: DisplayOptions
 
const retriesOr = opts.retries || 3 // 0 silently becomes 3 ❌
const retriesNullish = opts.retries ?? 3 // 0 stays 0 ✅
const titleNullish = opts.title ?? 'Untitled'

|| falls back on every falsy value: 0, '', false, NaN. ?? only falls back on null and undefined — which is exactly what "this was not provided" means. For optional numbers and booleans, || will eventually eat a legitimate value.

Optional tuple elements

Tuples take ? too, on trailing elements.

type LogEntry = [message: string, level?: 'warn' | 'error']
 
const plainLog: LogEntry = ['saved']
const warnLog: LogEntry = ['disk almost full', 'warn']

LogEntry has a length of 1 | 2, and reading index 1 gives you 'warn' | 'error' | undefined. Like parameters, optional tuple elements have to come last — a required element cannot follow an optional one.

Flipping optionality with Partial and Required

Marking fields one at a time gets old. Two utility types do it wholesale.

interface ServerConfig {
  host: string
  port: number
  tls?: boolean
}
 
type ConfigPatch = Partial<ServerConfig>
// { host?: string; port?: number; tls?: boolean }
 
type FullConfig = Required<ServerConfig>
// { host: string; port: number; tls: boolean }
 
const patch: ConfigPatch = { port: 8080 }
const resolved: FullConfig = { host: 'localhost', port: 8080, tls: false }

Partial makes every key optional, Required strips every ? away. Both are shallow — they touch the top level only, and nested objects keep whatever optionality they had.

Most of the time you want something in between: a few keys optional, the rest untouched. That composes out of the pieces you already have.

type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
 
type DraftConfig = PartialBy<ServerConfig, 'port'>
 
const draft: DraftConfig = { host: 'localhost' }

Omit drops the chosen keys, Partial<Pick<...>> adds them back as optional, and the intersection glues the two halves together. The PartialByKeys challenge is this exact type, built from scratch.

The lookup that may miss

Partial has a second job that is easy to miss: it is how you model a lookup table where not every key is filled in.

type Locale = 'en' | 'de' | 'fr'
type TranslationTable = Partial<Record<Locale, string>>
 
const greetings: TranslationTable = { en: 'Hello', de: 'Hallo' }
 
const frenchGreeting = greetings.fr // string | undefined

A bare Record<Locale, string> would demand all three keys and then promise every lookup returns a string. Wrapping it in Partial says what is actually true — some keys are missing, and a lookup may come back empty. The union you get back is the point, not an inconvenience.

Arrays and noUncheckedIndexedAccess

Array indexing is the one place TypeScript is optimistic by default.

declare const teamNames: string[]
 
const leadName = teamNames[0]

leadName is typed string, even though the array could be empty and the value could be undefined at runtime. The noUncheckedIndexedAccess flag fixes that: turn it on and every index read and index-signature read becomes T | undefined, which is the honest type. It is noisy on an existing codebase, and it catches a real class of bug.

Narrowing an optional value

Every optional read hands you a union, and every union needs narrowing before use.

function describeTls(config: ServerConfig): string {
  if (config.tls === undefined) {
    return 'tls not configured'
  }
  return config.tls ? 'tls on' : 'tls off'
}

Note the explicit === undefined rather than a truthiness check. config.tls can be false, and if (config.tls) would report a configured-but-disabled setting as "not configured" — the same trap as ||.

Narrowing works on nested optional objects too, and it survives across the rest of the block.

function readCity(place: Company): string {
  if (place.address === undefined) {
    return 'unknown'
  }
  return place.address.city
}

The catch is that this only holds while nothing can reassign the value. Move the read into a callback or an await, and TypeScript drops the narrowing, because it cannot prove the property is still there. Pull the value into a const first and the narrowing sticks.

When not to reach for ?

Optional is the default answer to "this field might not be there", and it is often the wrong one. Two cases in particular.

The first is a field that is required in some states and absent in others. Marking it optional collapses both states into one type, and every consumer pays for it with a check that cannot fail in half the call sites. A discriminated union models the states properly and the check becomes meaningful.

The second is the half-populated object that gets filled in over several steps. A type where every field is optional describes the intermediate states well and the finished object badly — nothing stops you passing a half-built value into code that needs a complete one. Keep the strict type, build up a Partial of it, and convert once at the boundary where it is genuinely complete.

? is right when absence carries no extra meaning. When absence means "we are in a different state", say that instead.

Wrapping up

? is one character doing four jobs, and the through-line is the same every time: it makes something skippable on the way in, and it hands you a | undefined union on the way out. Write side loose, read side strict.

The practical rules are short. Use ? when a key may be absent and | undefined when a caller must decide. Prefer a default parameter over an optional one when you have a sensible fallback. Use ?? and never || for optional numbers and booleans. And reach for Partial and Required before hand-editing a dozen ? marks.

If you want to practise, Optional Keys and Get Optional both ask you to pull the optional members back out of a type — which turns out to be much harder than putting them in.

Share this article

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your typescript optional knowledge to the test with these related challenges.

#90Optional Keys
Hard
#59Get Optional
Hard
#89Required Keys
Hard
#2757PartialByKeys
Medium
#2759RequiredByKeys
Medium
#4Pick
Easy

Related Concepts

Concepts that build on or relate to typescript optional.

InterfacesUnion TypesTypeScript Utility TypesTypeScript Function Types