[TS] any, unknown, never 무엇이 다른건가?!

이주영·2024년 3월 11일
0

typescript

목록 보기
1/4
post-thumbnail

카탈로그🔖 : any 다루기 > 모르는 타입의 값에는 any 대신 unknown을 사용하기

서론

이전 프로젝트에서 타입스크립트를 사용하면서 어려웠던 부분은 비동기 통신에 의한 결과 값을 타입으로 지정할 때였다. 어떤 값이 들어올지는 아는데 타입은 어떻게 지정해야 하는 건지 싶어 그 당시 any로 도배했던 기억이 있다. 이제야 unknown으로 지정하는 되는 걸 알게 됐지만 그럼에도 불구하고 정확히 모르는 것 같아 궁금해서 정리해보려고 한다.

총 4가지를 정리할 것이다.
1. 함수 반환값과 관련된 unknown 타입
2. 변수 선언과 관련된 unknown 타입
3. 단언문과 관련된 unknown 알아보기
4. unknown과 살짝 다른 타입

본론

1. 함수 반환값과 관련된 unknown

parseYAML 함수를 활용해서 알아가 보자. 우선 아래의 함수의 반환 타입을 any로 지정한다.

function parseYAML (yaml : string):any {
	//...
}

함수의 반환 타입으로 any를 사용하는 것은 좋지 않은 설계이기에 지양해야 하는 부분이니 호출하는 위치에서 반환 값 타입을 할당하는 것이 이상적으로 보인다.

interface Book {
	name : string;
	author : string;
}

function parseYAML (yaml : string):any {
	//...
}
const book : Book = parseYAML(`
	name : 로미오와 줄미엣
	author : 윌리엄 세익스피어
`);

하지만 함수의 반환 값에 타입 선언을 강제할 수 있다. 위의 코드에서 book 변수에 Book 타입을 삭제하면 자동으로 any가 추론되고 타입 오류는 알려주지 않지만 런타임 에러들이 등장할 것이다.

interface Book {
	name : string;
	author : string;
}

const book : Book = parseYAML(`
	name : 로미오와 줄미엣
	author : 윌리엄 세익스피어
`);

alert(book.title) // 오류를 알려주지 않는다. 객체 속성 검사를 해주지 않는다. 
book('read') // 오류 없다. 런타임에 "TypeError : book은 함수가 아니다"라고 알려준다.

그럼 어떻게 하면 될까?! 함수 정의부에서 함수의 반환 타입을 any에서 unknown으로 변경하는 것이 좋다.

function safeParseYAML (yaml : string): unknown {
	return parseYAML(yaml);
}
const book : Book = safeParseYAML(`
	name : 로미오와 줄미엣
	author : 윌리엄 세익스피어
`);
alert(book.title) // ~~ 개체가 'unknown' 형식입니다.
book('read') // ~~ 개체가 'unknown' 형식입니다. 

any보다 확실히 타입 체크를 해주는 것을 볼 수 있다.

이 부분을 이해하는게 중요하다. 집중 집중!

unknown 타입 제대로 이해하기

먼저 unknown 타입의 특징을 알아보기 전, any 타입의 위험성을 정확히 이해하고 그 기반으로 unknown과 다른 타입을 정리해보려고 한다.

any 타입 두 가지 특징이 있다. 코드를 예시로 설명해보면

  1. 어떠한 타입이든 any 타입에 할당 가능하다.
let value: any;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
  1. any 타입은 never 타입을 제외한 모든 타입에 할당 가능하다.
let value: any;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // OK
let value4: number = value; // OK
let value5: string = value; // OK
let value6: object = value; // OK
let value7: any[] = value; // OK
let value8: Function = value; // OK

?? 첫 번째와 두 번째가 동일한 의미가 아닌가 싶었다. 두 가지가 굉장히 헷갈렸는데 예시를 천천히 보니 이해가 됐다. 정리해 보면 첫 번째는 any 타입의 변수에 어떤 타입의 값이던 지정이 가능하다는 의미이고 두 번째는 any 타입의 변수를 다른 타입이 할당된 변수의 값으로 사용할 때를 말하고 있는 것이다. 미묘하지만 다르다는 것을 알게 됐다.

unKnown 타입은 any 타입의 2가지 문제중 첫번 째는 만족하지만 두 번째는 만족하지 않는다.

그렇다면 any 타입의 특징을 이해한대로 unknown 타입도 어렵지 않게 이해할 수 있게 됐다.

  1. 어떠한 타입이든 unknown 타입에 할당 가능하다. (O)
let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
  1. unknown 타입은 never 타입을 제외한 모든 타입에 할당 가능하다. (X)
let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

즉 unknown 타입은 오직 unknown과 any 타입이 지정된 변수에만 할당이 가능하다!! 이렇기에 any보다는 안전한 타입이라고 하는 것이다.

let value: unknown;
// 다른 타입에 할당하는 예제 (오류 발생)
let numberValue: number = value; // 오류: Type 'unknown' is not assignable to type 'number'.

// any 타입에 할당하는 예제 (가능)
let anyValue: any = value;

// 타입 확인 후 사용하는 예제
if (typeof value === 'number') {
    let numberValue: number = value; // 이제는 오류가 발생하지 않음
} else {
    console.log("값의 타입이 숫자가 아닙니다.");
}

never 타입은 두번 째 타입인 unknown 타입과 또 반대이다.

  1. 어떠한 타입이든 never 타입에 할당 가능하다. (X)
  2. never 타입은 모든 타입에 할당 가능하다. (O)

이미지를 활용해서 이해해보자

unknown은 모든 타입의 슈퍼셋으로 어떠한 타입이든 unknown 타입에 할당 가능하지만 다른 타입에 할당하진 못한다. 반대로 never 타입은 모든 타입의 서브셋 타입이므로 어떠한 타입이든 never 타입에 할당 가능하지 않지만 never 타입은 모든 타입에 할당이 가능하다.

unknown 타입인 채로 값을 사용하면 오류가 발생한다. 그렇기에 타입 단언을 통해 타입 체커를 보다 명확하게 할 수 있다.

그래서 타입 단언하여 반환값이 Book이라고 기대할 수 있게 할 수 있다.

// 이전 
function safeParseYAML (yaml : string): unknown {
	return parseYAML(yaml);
}
const book : Book = safeParseYAML(`
	name : 로미오와 줄미엣
	author : 윌리엄 세익스피어
`);
alert(book.title) // ~~ 개체가 'unknown' 형식입니다.
book('read') // ~~ 개체가 'unknown' 형식입니다. 

// 이후
function safeParseYAML (yaml : string): unknown {
	return parseYAML(yaml);
}
const book : Book = safeParseYAML(`
	name : 로미오와 줄미엣
	author : 윌리엄 세익스피어
`) as Book;

alert(book.title) // 'Book' 형식에 'title' 속성이 없습니다. 
book('read') // 이 식은 호출할 수 없다. 

2. 변수 선언과 관련된 unknown

어떤 값이 있지만 그 타입을 모를 때 unknown을 사용할 수 있다. 아래의 Feature 프로퍼티에 무엇이 들어올지 모른다고 가정해 본다.

interface Feature {
id? : string | number;
geometry : Geometry;
properties : unknown;
}

타입 단언문이 unknown에서 원하는 타입으로 변환하는 유일한 방법은 아니다. 다른 방법으로 instanceof를 체크한 후 unknown에서 원하는 타입으로 변환할 수 있다.

function processValue(val: unknown) {
	if(val instanceof Date) {
		val // 타입이 Date로 변화된다. 
	}
}

다른 방법으로는 사용자 정의 타입 가드도 unknown에서 원하는 타입으로 변환할 수 있다.

function isBook(val: unknown): val is Book {
	return (
		typeof(val) === 'object' && val !== null &&
		'name' in val && 'author' in val
	);
}

function processValue(val : unknown) {
	if(isBook(val)){
		val; //타입이 Book
	}
}

unknown 타입의 범위를 좁히기 위해선 노력이 많이 필요하다.
1. in 연산자에서 오류를 피하기 위해서 val이 객체임을 확인해야한다
2. typeof null === 'object' 이므로 별도로 val이 null이 아님을 확인해야한다.

Unknown 대신 제네릭 매개변수가 사용되는 경우도 있다.

function safeParseYAML<T>(yaml: string) :T {
	return parseYAML(yaml);
}

제네릭을 활용해서 위의 방식을 구현하는 것은 그리 좋은 방법이 아니라고 한다. 왜지?! 결국 타입 단언과 동일한 기능을 하기에 제네릭보다는 unknown을 반환하고 직접 단언문을 사용하거나 원하는 타입을 좁히도록 강제하는 것이 더욱 좋다고 한다.

3. 단언문과 관련된 unknown 알아보기

이중 단언문에서 any 대신 unknown을 사용할 수 있다.

declare const foo : Fpp;
let barAny = foo as any as Bar;
let barUnknown = foo as unknown as Bar;

barAny와 barUnknown 모두 기능적으로는 동일하지만 두 개의 단언문을 분리할 경우 unknown 형태가 더욱 안전하다고 한다. unknown의 경우는 분리되는 경우 오류를 발생하지만 any는 오류를 발생하지 않아 디버깅하기 어렵게 만든다.

4. unknown과 살짝 다른 타입

  1. object 또는 {} 는 unknown만큼 범위가 넓은 타입이지만 unknown보다는 좁은 범위이다.
    • {} 타입은 nullrhk undefined를 제외한 모든 값을 포함한다.
    • object 타입은 모든 non-primitive 타입으로 이루어졌다. 여기에 객체와 배열은 포함된다.

unknown 타입이 나오기 전에는 {} 타입이 일반적이었다고 말한다. null과 undefined가 불가능한 경우에만 unknown 대신 {} 타입을 사용하면 된다.

결론

  • unknown 타입은 any 타입을 대신하여 사용할 수 있는 안전한 타입이다. 어떠한 값이 있지만 타입을 알지 못할 때 unknown을 사용하면 된다.
  • 사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 unknown을 사용하면 된다.
profile
https://danny-blog.vercel.app/ 문제 해결 과정을 정리하는 블로그입니다.

0개의 댓글