Item 22: Understand Type Narrowing
May 10, 2024 ยท View on GitHub
Things to Remember
- 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.
Code Samples
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');
}
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
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
}
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
}
function contains(text: string, terms: string | string[]) {
const termList = Array.isArray(terms) ? terms : [terms];
// ^? const termList: string[]
// ...
}
const elem = document.getElementById('what-time-is-it');
// ^? const elem: HTMLElement | null
if (typeof elem === 'object') {
elem;
// ^? const elem: HTMLElement | null
}
function maybeLogX(x?: number | string | null) {
if (!x) {
console.log(x);
// ^? (parameter) x: string | number | null | undefined
}
}
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;
}
}
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
}
const formEls = document.querySelectorAll('.my-form *');
const formInputEls = [...formEls].filter(isInputElement);
// ^? const formInputEls: HTMLInputElement[]
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;
}
const nickname = nameToNickname.get(yourName);
let nameToUse: string;
if (nickname !== undefined) {
nameToUse = nickname;
} else {
nameToUse = yourName;
}
const nameToUse = nameToNickname.get(yourName) ?? yourName;
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'.
}
}
const obj: { value: string | number } = { value: 123 };
logLaterIfNumber(obj);
obj.value = 'Cookie Monster';