Item 22: Understand Type Narrowing
Understand how TypeScript narrows types based on conditionals and other types of control flow.
Use tagged/discriminated unions and user-defined type guards to help the process of narrowing.
Think about whether code can be refactored to let TypeScript follow along more easily.
const elem = document . getElementById ( 'what-time-is-it' ) ;
// ^? const elem: HTMLElement | null
if ( elem ) {
elem . innerHTML = 'Party Time' . blink ( ) ;
// ^? const elem: HTMLElement
} else {
elem
// ^? const elem: null
alert ( 'No element #what-time-is-it' ) ;
}
💻 playground
const elem = document . getElementById ( 'what-time-is-it' ) ;
// ^? const elem: HTMLElement | null
if ( ! elem ) throw new Error ( 'Unable to find #what-time-is-it' ) ;
elem . innerHTML = 'Party Time' . blink ( ) ;
// ^? const elem: HTMLElement
💻 playground
function contains ( text : string , search : string | RegExp ) {
if ( search instanceof RegExp ) {
return ! ! search . exec ( text ) ;
// ^? (parameter) search: RegExp
}
return text . includes ( search ) ;
// ^? (parameter) search: string
}
💻 playground
interface Apple { isGoodForBaking : boolean ; }
interface Orange { numSlices : number ; }
function pickFruit ( fruit : Apple | Orange ) {
if ( 'isGoodForBaking' in fruit ) {
fruit
// ^? (parameter) fruit: Apple
} else {
fruit
// ^? (parameter) fruit: Orange
}
fruit
// ^? (parameter) fruit: Apple | Orange
}
💻 playground
function contains ( text : string , terms : string | string [ ] ) {
const termList = Array . isArray ( terms ) ? terms : [ terms ] ;
// ^? const termList: string[]
// ...
}
💻 playground
const elem = document . getElementById ( 'what-time-is-it' ) ;
// ^? const elem: HTMLElement | null
if ( typeof elem === 'object' ) {
elem ;
// ^? const elem: HTMLElement | null
}
💻 playground
function maybeLogX ( x ?: number | string | null ) {
if ( ! x ) {
console . log ( x ) ;
// ^? (parameter) x: string | number | null | undefined
}
}
💻 playground
interface UploadEvent { type : 'upload' ; filename : string ; contents : string }
interface DownloadEvent { type : 'download' ; filename : string ; }
type AppEvent = UploadEvent | DownloadEvent ;
function handleEvent ( e : AppEvent ) {
switch ( e . type ) {
case 'download' :
console . log ( 'Download' , e . filename ) ;
// ^? (parameter) e: DownloadEvent
break ;
case 'upload' :
console . log ( 'Upload' , e . filename , e . contents . length , 'bytes' ) ;
// ^? (parameter) e: UploadEvent
break ;
}
}
💻 playground
function isInputElement ( el : Element ) : el is HTMLInputElement {
return 'value' in el ;
}
function getElementContent ( el : HTMLElement ) {
if ( isInputElement ( el ) ) {
return el . value ;
// ^? (parameter) el: HTMLInputElement
}
return el . textContent ;
// ^? (parameter) el: HTMLElement
}
💻 playground
const formEls = document . querySelectorAll ( '.my-form *' ) ;
const formInputEls = [ ...formEls ] . filter ( isInputElement ) ;
// ^? const formInputEls: HTMLInputElement[]
💻 playground
const nameToNickname = new Map < string , string > ( ) ;
declare let yourName : string ;
let nameToUse : string ;
if ( nameToNickname . has ( yourName ) ) {
nameToUse = nameToNickname . get ( yourName ) ;
// ~~~~~~ Type 'string | undefined' is not assignable to type 'string'.
} else {
nameToUse = yourName ;
}
💻 playground
const nickname = nameToNickname . get ( yourName ) ;
let nameToUse : string ;
if ( nickname !== undefined ) {
nameToUse = nickname ;
} else {
nameToUse = yourName ;
}
💻 playground
const nameToUse = nameToNickname . get ( yourName ) ?? yourName ;
💻 playground
function logLaterIfNumber ( obj : { value : string | number } ) {
if ( typeof obj . value === "number" ) {
setTimeout ( ( ) => console . log ( obj . value . toFixed ( ) ) ) ;
// ~~~~~~~
// Property 'toFixed' does not exist on type 'string | number'.
}
}
💻 playground
const obj : { value : string | number } = { value : 123 } ;
logLaterIfNumber ( obj ) ;
obj . value = 'Cookie Monster' ;
💻 playground