현록

TypeScript any와 unknown 차이

anyunknown은 어떤 값이든 받을 수 있는 TypeScript 타입이다.
하지만 그 값을 사용하는 시점의 검사 방식은 다르다.
any는 타입 검사를 사실상 끄고, unknown은 값의 타입을 확인한 다음 사용하도록 강제한다.
외부에서 들어와 타입을 알 수 없는 값에는 unknown이 더 안전한 기본값이다.

any와 unknown의 공통점

문자열, 숫자, boolean, 객체, 함수 등 모든 종류의 값을 anyunknown에 할당할 수 있다.
따라서 두 타입의 차이는 어떤 값을 받는지보다 받은 값으로 무엇을 할 수 있는지에서 나타난다.

any는 컴파일러에게 해당 값의 타입 검사를 제거하라고 알리는 탈출구에 가깝다.
unknown은 정보가 아직 확인되지 않았다는 상태를 타입으로 표현한다.

타입 검사와 할당 차이

any에서는 속성 접근, 메서드 호출, 구체 타입으로의 할당이 검사 없이 통과할 수 있다.
unknown에서 같은 작업을 하려면 먼저 타입을 좁혀야 한다.

declare const anyValue: any
declare const unknownValue: unknown

anyValue.trim() // 컴파일 통과
unknownValue.trim() // 오류: 사용 전에 타입 확인 필요

const nameFromAny: string = anyValue // 컴파일 통과
const nameFromUnknown: string = unknownValue // 오류

anyValue.trim()은 실제 값에 호출 가능한 trim()이 없어도 컴파일 오류가 없다.
문제는 런타임으로 미뤄진다.
unknown이 만드는 컴파일 오류는 외부 값을 검증하라는 신호다.

unknown 타입 좁히기

typeof, instanceof, 동등성 비교, in 연산자 같은 JavaScript 조건문으로 unknown을 더 구체적인 타입으로 좁힐 수 있다.
조건 안에서는 TypeScript가 확인된 타입의 속성과 메서드를 허용한다.

function formatValue(value: unknown): string {
  if (typeof value === 'string') {
    return value.trim()
  }

  if (value instanceof Error) {
    return value.message
  }

  return String(value)
}

여러 곳에서 반복해 검증할 구조는 사용자 정의 타입 가드로 분리할 수 있다.
반환 타입의 value is User는 함수가 true를 반환한 분기에서 valueUser로 좁힌다.

외부 데이터 경계

JSON, 사용자 입력, 제3자 라이브러리 결과는 소스 코드에 적힌 타입만으로 실제 구조가 보장되지 않는다.
경계에서 unknown으로 받고 구조를 검증한 뒤 도메인 타입으로 넘기면 안전하지 않은 값의 범위를 줄일 수 있다.

type User = {
  id: number
  name: string
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof value.id === 'number' &&
    'name' in value &&
    typeof value.name === 'string'
  )
}

function safeParse(text: string): unknown {
  return JSON.parse(text)
}

const payload = safeParse('{"id":1,"name":"Kim"}')

if (isUser(payload)) {
  console.log(payload.name)
}

unknown은 런타임 검증을 자동으로 수행하지는 않는다.
as User로 강제 단언하면 검증 없이 컴파일러를 우회하게 되므로 타입 가드의 실제 조건이 필요하다.

객체 타입을 type으로 정의할지 interface로 정의할지의 기준은 TypeScript type과 interface 차이에 정리했다.

any가 필요한 제한적 범위

JavaScript 프로젝트를 TypeScript로 점진적으로 옮기거나 타입 정의가 없는 라이브러리를 연결할 때 any가 임시 탈출구가 될 수 있다.
이때도 any를 애플리케이션 전체로 전파하지 말고 하나의 어댑터나 함수 내부에 격리하는 편이 좋다.

단순히 컴파일 오류를 없애기 위해 any를 넣으면 오류 발견 시점만 런타임으로 옮겨진다.
타입을 아직 모를 뿐이라면 unknown을 사용하고, 구조를 알게 된 지점에서 좁힌다.

컴파일러 설정

noImplicitAny를 켜면 TypeScript가 타입을 추론하지 못해 암시적으로 any가 된 위치를 오류로 표시한다.
useUnknownInCatchVariables를 켜면 타입 주석이 없는 catch 변수의 기본 타입이 unknown으로 바뀐다.
strict는 이 옵션을 포함한 여러 엄격한 타입 검사 옵션을 한 번에 활성화한다.

try {
  runExternalCode()
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error(error.message)
  } else {
    console.error('Unknown error', error)
  }
}

JavaScript에서는 어떤 값이든 throw할 수 있으므로 catch 변수가 항상 Error의 인스턴스라고 가정하면 안 된다.
검증을 통과한 값만 사용하게 만드는 것이 unknown의 핵심 역할이다.

참고 자료

관련 포스트
JavaScript 얕은 복사와 깊은 복사 차이 thumbnail
JavaScript 얕은 복사와 깊은 복사 차이
JavaScript 얕은 복사와 깊은 복사의 차이를 객체 참조, spread, structuredClone, JSON 복사의 제약으로 정리합니다.
JavaScript find와 filter 차이 thumbnail
JavaScript find와 filter 차이
JavaScript find와 filter의 차이를 반환값, 순회 종료, 객체 참조, 일치 항목이 없을 때의 처리 기준으로 정리합니다.
JavaScript null과 undefined 차이 thumbnail
JavaScript null과 undefined 차이
JavaScript null과 undefined의 차이를 값이 생기는 경로, 비교, nullish 처리, JSON 직렬화, API 선택 기준으로 정리합니다.
TypeScript type과 interface 차이 thumbnail
TypeScript type과 interface 차이
TypeScript의 type과 interface 차이를 객체 모델링, 유니온, 선언 병합, extends와 교차 타입의 충돌 처리, 실무 선택 기준으로 나누어 정리합니다.
JavaScript ==와 === 차이 thumbnail
JavaScript ==와 === 차이
JavaScript ==와 ===의 차이를 암시적 타입 변환, null과 undefined, boolean과 문자열, 객체 참조, NaN, 비교 연산자 선택 기준으로 정리합니다.
marked.js 사용법과 기본 설정 thumbnail
marked.js 사용법과 기본 설정
marked.js 사용법을 초보자 기준으로 정리합니다. marked.parse와 parseInline, gfm과 breaks 옵션, 브라우저 출력, HTML sanitize 주의점까지 함께 봅니다.
JavaScript var let const 차이 thumbnail
JavaScript var let const 차이
JavaScript var, let, const 차이를 함수와 블록 스코프, 재할당과 재선언, 호이스팅과 TDZ, 반복문 closure, 객체 변경 기준으로 정리합니다.
URL path parameter와 query string 차이 thumbnail
URL path parameter와 query string 차이
URL path parameter와 query string의 차이를 초보자 기준으로 정리합니다. 리소스 식별자, 조회 조건, URLSearchParams, 인코딩, API 설계 기준을 함께 봅니다.
쿠키와 localStorage 차이 thumbnail
쿠키와 localStorage 차이
쿠키와 localStorage의 차이를 초보자 기준으로 정리합니다. 서버 자동 전송, JavaScript 접근, 만료 시간, HttpOnly, Secure, SameSite, 로그인 상태 저장 기준을 함께 봅니다.
script async와 defer 차이 thumbnail
script async와 defer 차이
HTML script 태그의 기본 동작, async와 defer의 다운로드 방식, 실행 순서, DOMContentLoaded와의 관계를 초보자 기준으로 정리합니다.
CORS 에러와 해결 기준 thumbnail
CORS 에러와 해결 기준
브라우저의 Same-Origin Policy, CORS 응답 헤더, preflight, 서버에서 해결해야 하는 이유를 초보자 기준으로 정리합니다.
fetch와 axios 차이 thumbnail
fetch와 axios 차이
fetch와 axios의 차이를 초보자 기준으로 정리합니다. 설치 여부, JSON 처리, HTTP 에러 처리, timeout, interceptor, 언제 어떤 방식을 쓰면 좋은지 함께 봅니다.
JavaScript map과 forEach 차이 thumbnail
JavaScript map과 forEach 차이
JavaScript의 map과 forEach 차이를 반환값, 새 배열, 부수 효과, 원본 변경, 비동기 콜백, Promise.all 사용 기준으로 나누어 정리합니다.
Vue 3 입문기 thumbnail
Vue 3 입문기
Vue의 메인 버전이 3가 된지 꽤 됐다. 작성 당시에는 Nuxt 3가 RC 단계였기 때문에 실서비스에 바로 적용하기에는 조심스러웠다. 그래서 궁금하고 심심하던 참에 간단한 Todo App을 만들어봤다.
marked.js renderer custom 하기 thumbnail
marked.js renderer custom 하기
marked.js renderer custom 방법을 정리합니다. Renderer의 역할, image와 link 렌더러 커스텀, alt와 rel 처리, sanitize-html 같은 HTML 정화 주의점까지 함께 봅니다.
가독성 있게 상수 넣기 thumbnail
가독성 있게 상수 넣기
오늘 회사 동료의 PR 리뷰 과정에서 좋은 기능을 공유해주셔서 TIL로 남겨봅니다. 아래와 같이 상수에 언더바(_)로 콤마처럼 구분을 시켜줄 수 있습니다. 앞으로 깔끔하고 좋은 코드를 작성하기 위해 자주 사용해야겠습니다 :)
TS에서 generic optional 하게 설정하기 thumbnail
TS에서 generic optional 하게 설정하기
오늘 next에서 `getStaticProps`와 `getLayout` 패턴을 함께 사용할 때, typescript generic을 넘겨주는 작업을 하고 있었는데, 기본값이 없다보니 기존 코드에 에러가 발생했었다. 이를 해결하기 위해 찾아보니 단순히 아래 예시처럼 `= {}`을 추가해주면 해결된다고 한다.
Backend에서 API Response가 snake_case인 경우엔? thumbnail
Backend에서 API Response가 snake_case인 경우엔?
안녕하세요. 프론트엔드 개발자의 경우, 가끔 백엔드의 API Response 값이 snake_case일 경우 어떻게 관리할지에 대해 고민에 빠지게 됩니다. 저도 오늘 같은 상황을 겪게 되었는데,  이번엔 네이밍 컨벤션을 맞춰주기로 했습니다. 컨벤션을 맞추는 데에는 여러가지 방법이 있겠지만, 고민 끝에 저는 axios의 interceptors를 통해 해결을 해보았습니다.
Promise 다루기 (feat. 병렬실행, 순차실행) thumbnail
Promise 다루기 (feat. 병렬실행, 순차실행)
오늘은 Promise를 통해 구문을 동기 처리 할 때, 여러 Promise들을 다루는 법을 소개해보겠습니다. Javscript를 작성하다 보면, 가끔 여러 Promise들을 다룰 때가 있습니다. 필자도 Nodejs 서버에서 동시에 여러 쿼리를 실행할 때 자주 마주쳤었는데요. 오늘은 어떻게 하면 Promise들을 유연하게 다룰 수 있는지 알아보겠습니다. 시작하기 앞서, 네 가지 Promise를 선언하고, 그들을 하나의 Array에 묶어보겠습니다.