Nonnull 어서션
null이 될 수 있는 특별한 상황(T | null 또는 T | null | undefined 타입)을 대비해 TypeScript는 어떤 값의 타입이 null이나 undefined가 아니라 T임을 단언하는 특수 문법을 제공합니다.
몇 가지 상황에서 이 기능을 활용할 수 있습니다.
예를 들어 웹 앱에서 다이얼로그를 보여주거나 숨기는 프레임워크를 개발했다고 가정합니다.
각 다이얼로그는 고유의 ID를 가지며 이 ID로 다이얼로그의 DOM 노드 참조를 얻을 수 있습니다.
DOM 노드에서 다이얼로그가 사라지면 ID를 삭제해서 DOM 안에 다이얼로그가 더 이상 존재하지 않음을 알립니다.
type Dialog = {
id?: string
}
function closeDialog(dialog: Dialog) {
if(!dialog.id){ // ①
return
}
setTimeout(() => // ②
removeFromDOM(
dialog,
document.getElementById(dialog.id)
// ③ 'string | undefined' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없습니다.
// 'undefined' 형식은 'string' 형식에 할당할 수 없습니다.ts(2345)
)
)
}
function removeFromDOM(dialog: Dialog, element: Element){
element.parentNode.removeChild(element)
// ④ 'element.parentNode'은(는) 'null'일 수 있습니다.ts(18047)
delete dialog.id
}
- 다이얼로그가 이미 삭제되어서 id가 없다면 일찍 반환합니다.
- 이벤트 루프의 다음 차례 때 다이얼로그를 삭제하도록 하여 dialog에 의존하는 다른 코드가 마무리 작업을 실행할 수 있는 기회를 제공합니다.
- 화살표 함수 내부이므로 유효 범위가 바뀌었습니다. 1과 3 사이에서 어떤 코드가 dialog를 변경해도 TypeScript는 알 수 없으므로 1에서 시행한 정제가 무효화됩니다. 또한 dialog.id가 정의되어 있으면 그 ID에 해당하는 요소가 DOM에 반드시 존재한다는 사실을 프레임워크를 이렇게 설계했으므로 알고 있지만 TypeScript입장에선 document.getElementById를 호출하면 HTMLElement | null을 반환한다는 사실만 알고 있을 뿐입니다.
- 마찬가지로 DOM에 다이얼로그가 있으며 부모 DOM 노드도 있다는 사실을 알고 있지만 TypeScript는 element.parentNode가 Node | null이라는 사실만 알 뿐입니다.
필요한 모든 곳에 if (_ === null)을 추가해 이 문제를 해결할 수 있습니다.
대상이 null인지 여부를 확신할 수 없다면 올바른 해법입니다.
하지만 대상이 null | undefined가 아님을 확신하는 경우라면 TypeScript가 제공하는 특별 문법을 활용할 수 있습니다.
type Dialog = {
id?: string
}
function closeDialog(dialog: Dialog){
if(!dialog.id){
return
}
setTimeout(() =>
removeFromDOM(
dialog,
document.getElementById(dialog.id!)!
)
)
}
function removeFromDOM(dialog: Dialog, element: Element){
element.parentNode!.removeChild(element)
delete dialog.id
}
간간이 보이는 nonnull 어서션 연산자(!)로 document.getElementById의 호출 결과인 dialog.id와 element.parentNode가 정의되어 있음을 TypeScript에게 알려주었습니다.
null이거나 undefined일 수 있는 타입 뒤에 nonull 어서션이 따라오면 TypeScript는 가령 T | null | undefined로 정의된 타입은 T로, number | string | null로 정의된 타입은 number | string으로 바꿉니다.
nonnull 어서션을 너무 많이 사용하고 있다는 생각이 들면 코드를 리팩터링해야 한다는 징후일 수 있습니다.
예를 들어 Dialog를 두 타입의 유니온으로 분리해 어서션을 제거할 수 있습니다.
type VisibleDialog = {id: string}
type DestoryDialog = {}
type Dialog = VisibleDialog | DestoryDialog
그리고 이 유니온을 이용하도록 closeDialog 코드를 수정합니다.
type VisibleDialog = { id: string };
type DestoryDialog = {};
type Dialog = VisibleDialog | DestoryDialog;
function closeDialog(dialog: Dialog) {
if (!("id" in dialog)) {
return;
}
setTimeout(() => removeFromDOM(dialog, document.getElementById(dialog.id)!));
}
function removeFromDOM(dialog: VisibleDialog, element: Element) {
element.parentNode!.removeChild(element);
delete dialog.id;
}
dialog에 id 프로퍼티가 정의되었음을 확인한 뒤로는 화살표 함수 내부에서도 TypeScript는 dialog의 참조가 바뀌지 않았음을 압니다.
즉, 화살표 함수 내부의 dialog는 외부의 dialog와 같은 값이므로, 정제 결과가 계속 이어집니다.
'👶 TypeScript' 카테고리의 다른 글
이름 기반 타입 흉내내기 (0) | 2023.01.22 |
---|---|
탈출구 - 확실한 할당 어서션 (0) | 2023.01.22 |
탈출구 - 타입 어서션 (0) | 2023.01.21 |
조건부 타입 - 내장 조건부 타입들 (0) | 2023.01.21 |
조건부 타입 - infer 키워드 (0) | 2023.01.21 |