너굴 개발 일지

[TypeScript] any, unknown은 언제 사용해야 할까? 본문

TypeScript

[TypeScript] any, unknown은 언제 사용해야 할까?

너굴냥 2023. 12. 25. 19:42

TypeScript any, unknown 타입의 차이를 공부하다가 알게 된 점을 정리한 글입니다.

목차

서론

본론

결론


서론

아래 설명에 앞서 아래 타입은 JS 자료형에서 제시되지 않은 독자적인 타입 시스템에 포함된 타입이다.

물론 TS의 타입 시스템이 내포하고 있는 개념은 모두 JS에서 기인되었지만 단지 JS로 표현할 수단과 필요성이 없었을 뿐으로 앞으로 소개한 모든 타입 시스템은 TS에만 존재하지만 그 개념은 JS에서 기인한 타입 시스템이라는 점을 인지해야 한다.

 

any 타입

JS에 존재하는 모든 값을 오류 없이 받을 수 있다.

TS는 동적 타이핑을 가진 JS에 정적 타이핑을 적용하는 것이 주된 목적이므로 any 타입을 변수에 할당하는 것은 지양할 패턴이다.

let state: any;

state = { value: 0 };   // 객체를 할당해도
state = 100;    // 숫자를 할당해도
state.foo.bar = () => console.log("this is any type"); // 심지어 중첩 구조로 들어가 함수를 할당해도 문제 없다

따라서 타입스크립트의 컴파일러 설정을 커스텀할 수 있는 tsconfig.json 파일에서 noImplictAny 옵션을 활성화하면 타입이 명시되지 않은 변수의 암묵적인 any 타입에 대한 경고를 발생시킬 수 있다.

// tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true,
   }
 }

 

any 타입을 사용할 수 밖에 없는 케이스

1. 개발 단계에서 임시로 값을 지정해야 할 때

개발 과정에서 추후 값이 변경될 가능성이 있거나 아직 세부 항목에 대한 타입이 확정되지 않은 경우 any 타입을 사용할 수 있다.

하지만 지나치게 any 타입을 남발하면 타입 안정성을 저해할 수 있고 세부 스펙이 나온 시점에 any 타입을 다른 타입으로 바꾸는 과정이 누락되는 실수를 할 수 있기에 주의해야 한다.

2. 어떤 값을 받아올지 또는 넘겨줄지 정할 수 없을 때

예를 들면 API 요청 및 응답 처리, 콜백 함수 전달, 타입이 정제되지 않아 파악이 힘든 외부 라이브러리 등을 사용할 때 어떤 인자를 주고 받을 지 특정하기 힘들다. 이런 경우에는 열린 타입(any 타입)을 선언해야 할 수 있다.

type FeedbackModalParams = {
    show: boolean
    content: string
    cancelButtonText?: string
    confirmButtonText?: string
    beforeOnClose?: () => void
    action?: any
}

위 예시는 FeedbackModalParams는 피드백을 나타내는 모달창을 그릴 때 사용되는 인자를 나타내는 타입으로 이 중 action 속성은 모달 창을 그릴 때 실행될 함수를 의미한다.

모달창을 화면에 그릴 때 다양한 범주의 액션에 따라 인자의 개수나 타입을 일일이 명시하기 힘들 수 있기에 any 타입을 사용해 다양한 액션 함수를 전달할 수 있다.

3. 값을 예측할 수 없을 때 암묵적으로 사용

외부 라이브러리나 웹 API의 요청에 따라 다양한 값을 반환하는 API가 존재할 수 있다.

대표적인 예로, 브라우저의 Fetch API의 일부 메서드는 요청 이후 응답을 특정 포맷으로 파싱하는데 이때 반환 타입이 any로 매핑되어 있다.

async function load() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const data = await response.json(); // return Promise<any>
  return data;
}

 

그렇다면 위 세 가지 케이스의 경우, any 타입을 사용하면 안전할까?

위 예시에서 FeedbackModalParamsaction 속성의 경우, 실수 또는 악의적으로 함수가 아닌 값을 넘기더라도 타입스크립트는 이를 에러로 간주하지 않지만 실제 런타임에서는 심각한 오류가 발생할 수 있다.

결론적으로 any 타입은 개발자에게 편의성과 확장성을 제공하기도 하지만 해당 값을 컨트롤하려면 파악해야 할 정보도 많다.

즉, 도구의 도움을 받을 수 없는 상태에서 온전히 개발자 스스로 책임져야 함을 의미한다.

 

unknown 타입

unknown 타입은 이름처럼 무엇이 할당될지 아직 모르는 상태의 타입을 말한다.

any 타입과 유사하게 모든 타입의 값이 할당될 수 있다. 그러나 any를 제외한 다른 타입으로 선언된 변수에는 unknown 타입 값을 할당할 수 없다.

let unknownValue: unknown;

unknownValue = 100; // any 타입과 유사하게 숫자이든
unknownValue = "hello" // 문자열이든
unknownValue = () => console.log("this is any type"); // 함수이든 상관없이 할당 가능하지만

let anyValue: any = unknownValue // (O) any 타입으로 선언된 변수를 제외한 다른 변수는 모두 할당 불가
let stringValue: string = unknownValue // (X) Type 'unknown' is not assignable to type 'string'.ts(2322)
let numberValue: number = unknownValue // (X) Type 'unknown' is not assignable to type 'number'.ts(2322)

 

 

any 타입과 unknown 타입의 차이

얼핏 보면 비슷해보이지만 차이점이 존재하는데 any 타입은 어떤 타입(never 제외)으로도 할당이 가능하며 unknown 타입은 any 타입 외에 다른 타입으로 할당이 불가능하다.

그러면 왜 굳이 unknown 타입을 추가했을까?

unknown 타입은 타입스크립트 3.0이 릴리스될 때 추가되었는데 기존 타입 시스템에서 부족한 부분을 보완하기 위해 등장했다.

let state: any;
state = () => console.log("this is any type");
state(); // (O)

let unknownValue: unknown;
unknownValue = () => console.log("this is unknown type");
unknownValue() // error: 'unknownValue' is of type 'unknown' ts(18046)

위 예시를 보면 any 타입의 state 변수는 함수로 잘 동작하는데 unknown 타입의 변수를 함수로 할당 후 실행하려 하면 에러가 발생한다.

unknown 타입은 어떤 타입이 할당되었는지 알 수 없음을 나타내기에 unknown 타입으로 선언된 변수는 값을 가져오거나 내부 속성에 접근할 수 없다. 이는 unknown 타입으로 할당된 변수는 어떤 값이든 올 수 있음을 의미하는 동시에 개발자에게 엄격한 타입 검사를 강제하는 의도를 담고 있다.

let unknownValue: unknown;
unknownValue = () => console.log("this is unknown type");
if (typeof unknownValue === "function") {
  unknownValue();
}

결론적으로 unknown 타입은 any 타입과 유사하지만 타입 검사를 강제하고 타입이 식별된 후에 사용할 수 있기 때문에 any 타입보다 더 안전하다.

따라서 데이터 구조를 파악하기 힘들 때 any 타입 대신 unknown 타입으로 대체해서 사용하는 방법이 권장된다.

 

타입 검사말고 as unknown as를 사용하는 건 어떨까?

TypeScript에서는 타입 단언(Type Assertion)이라는 매커니즘이 있으며 개발자가 컴파일러에게 내가 너보다 타입에 대해 더 잘 알고 있고, 나의 주징에 대해 의심하지 말라고 하는 것과 같다. 

타입 단언은 as와 같은 키워드로도 적용할 수 있으며 예를 들면 아래처럼 이중 표명을 통해 더 구체적인 타입 정보를 사용할 수 있다.

function handler (event: Event) {
    let mouseEvent = event as MouseEvent;
}

종종 강제 타입 캐스팅을 위해 as unknown as를 사용하는 경우도 있는데 타입 검사말고 이걸 사용해도 괜찮지 않을까?

타입 단언과 타입 캐스팅은 같은 의미인가?
타입 단언은 타입을 변경한다는 사실 때문에 타입 캐스팅과 비슷하게 느껴질 수 있다. 타입 단언이 타입 캐스팅이라고 불리지 않는 이유는 런타임에 영향을 미치지 않기 때문이다. 타입 캐스팅은 컴파일타임과 런타임에서 모두 타입을 변경시키지만 타입 단언은 오직 컴파일타임에서만 타입을 변경시킨다.
const num = "string" as unknown as number;

위 예시는 말도 안되는 타입 선언이지만 코드 상에서 에러가 발생하진 않는다.

기본적으로 A as BAB에 할당하거나 BA가 할당할 수 있는 경우 동작한다.

따라서 "string" as unknownunknown에는 모든 타입을 할당할 수 있기에 "string" 문자열은 unknown이 된다.

두번째인 unknown as number도 마찬가지로 unknown에는 모든 타입이 할당될 수 있기에 unknown에 할당된다.

결론적으로 as unknown as도 any와 다를 바 없기 때문에 타입 안전성을 잃게 된다. 따라서 unknown 타입의 변수를 사용한다면 typeof, instanceof, in, is와 같은 연산자를 활용한 타입 가드가 필수적이다.

 

결론

any, unknown 타입으로 선언된 변수는 모든 타입의 값을 할당할 수 있지만 unknown의 경우, 타입 검사를 강제하고 타입이 식별된 후 값을 가져오거나 내부 속성에 접근할 수 있다.

따라서 타입을 알기 어려운 데이터를 다루는 경우, any보다 타입 안정성이 강한 unknown을 사용하는 것이 좋으며 타입 검사는 필수적이다.

 


참고 자료

https://product.kyobobook.co.kr/detail/S000210716282?utm_source=google&utm_medium=cpc&utm_campaign=googleSearch&gad_source=1&gclid=Cj0KCQiA7aSsBhCiARIsALFvovwcBD-erJzQXys-r2sIjU2EYfsx_c8Hvdeuiaq7gSz7IE_tnWjw6ZwaAkEJEALw_wcB

https://www.typescriptlang.org/ko/docs/handbook/2/everyday-types.html

https://stackoverflow.com/questions/69399211/typescript-why-does-as-unknown-as-x-work

https://radlohead.gitbook.io/typescript-deep-dive/type-system/type-assertion