Lesson 10 of 13 · 0 of 1 exercises solved
Narrowing & Type Guards
Learn how TypeScript narrows a such as | number to a single type by following your if statements and checks. This is how you work safely with values that can be more than one type.
function format(value: string | number | Date): string {
// typeof narrowing
if (typeof value === 'string') {
return value.toUpperCase() // value: string here
}
if (typeof value === 'number') {
return value.toFixed(2) // value: number here
}
// instanceof narrowing
return value.toISOString() // value: Date — the only option left
}
// truthiness narrowing
function greet(name?: string) {
if (!name) return 'Hello, stranger'
return `Hello, ${name.trim()}` // name: string
}
// "in" narrowing for object shapes
type Bird = { fly: () => void }
type Fish = { swim: () => void }
function move(animal: Bird | Fish) {
if ('fly' in animal) animal.fly()
else animal.swim()
}
// Discriminated unions — the single most useful modeling pattern in TS.
// Every member shares a literal "tag" field the compiler can switch on.
type Result<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function render(result: Result<string[]>): string {
switch (result.status) {
case 'loading': return 'Spinner...'
case 'success': return result.data.join(', ') // .data exists only here
case 'error': return result.error.message // .error exists only here
}
}
// Exhaustiveness checking: if someone adds a new variant, this fails to compile.
function assertNever(x: never): never {
throw new Error('Unhandled case: ' + JSON.stringify(x))
}
function renderStrict(result: Result<string[]>): string {
switch (result.status) {
case 'loading': return 'Spinner...'
case 'success': return result.data.join(', ')
case 'error': return result.error.message
default: return assertNever(result)
}
}
// Custom type guards: a function whose return type is "arg is T"
type User = { id: number; email: string }
function isUser(value: unknown): value is User {
return (
typeof value === 'object' && value !== null &&
'id' in value && typeof (value as User).id === 'number' &&
'email' in value && typeof (value as User).email === 'string'
)
}
async function loadUser(): Promise<User> {
const data: unknown = await fetch('/api/me').then(r => r.json())
if (!isUser(data)) throw new Error('API returned an unexpected shape')
return data // data: User from here on
}
// Assertion functions throw instead of returning a boolean
function assertDefined<T>(v: T | null | undefined, name: string): asserts v is T {
if (v == null) throw new Error(name + ' is required')
}
function process(input?: string) {
assertDefined(input, 'input')
return input.length // input: string — no ? needed
}
Practice
Write a type guard isString(value: unknown): value is string, then formatValue(value: unknown): string that returns strings upper-cased, numbers with two decimals, as their length in brackets, and "?" for anything else — narrowing at each step.
Exercise
3 checksNarrowing unknown
function isString(value) {
return false
}
function formatValue(value) {
return "?"
}
console.log(formatValue("hello"), formatValue(3.14159), formatValue([1, 2, 3]), formatValue(null))
Press Run to try your code, then Check answer to grade it.Check yourself
Why prefer unknown over any for a value you have not inspected yet?
Finished reading?1 exercise still open. You can come back to it later.
Tip: use the ← and → keys to move between lessons.