👶 TypeScript

탈출구 - Nonnull 어서션

개발자 린다씨 2023. 1. 22. 10:00
반응형

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
}
  1. 다이얼로그가 이미 삭제되어서 id가 없다면 일찍 반환합니다.
  2. 이벤트 루프의 다음 차례 때 다이얼로그를 삭제하도록 하여 dialog에 의존하는 다른 코드가 마무리 작업을 실행할 수 있는 기회를 제공합니다.
  3. 화살표 함수 내부이므로 유효 범위가 바뀌었습니다. 1과 3 사이에서 어떤 코드가 dialog를 변경해도 TypeScript는 알 수 없으므로 1에서 시행한 정제가 무효화됩니다. 또한 dialog.id가 정의되어 있으면 그 ID에 해당하는 요소가 DOM에 반드시 존재한다는 사실을 프레임워크를 이렇게 설계했으므로 알고 있지만 TypeScript입장에선 document.getElementById를 호출하면 HTMLElement | null을 반환한다는 사실만 알고 있을 뿐입니다. 
  4. 마찬가지로 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와 같은 값이므로, 정제 결과가 계속 이어집니다.

반응형