[타입스크립트] 타입스크립트에만 있는 타입들 (any, unknown, ...etc)
요즘 올라오는 채용공고를 보면 이제는 타입스크립트가 기본기의 일종이 되어버린게 아닌가하는 생각이 들 정도로 많이 포함되어 있다.
자바스크립트는 본래 간단한 돔조작을 하기 위한 용도로 빠르게 개발되었고 유연함이 강점이었는데, 현대에서는 워낙 다양하고 복잡한 애플리케이션을 만드는데에도 쓰이다보니 이런 유연함이 발목을 잡는 시대가 온 것 같다고 느낀다.
자바스크립트로 프로그래밍에 입문한 주변 지인들은 대게 타입스크립트를 성가신 친구쯤으로 생각하는 사람도 있는 것 같지만 개인적으로는 자바같은 정적 타이핑 언어로 프로그래밍에 입문했기에 이러한 흐름이 오히려 반갑게 느껴지기도 한다.
암튼 요새 좋은 기회로 타입스크립트 책을 선물받아서 읽을 기회가 생겼는데 이 참에 새롭게 알게 된 내용을 아름아름 기록해보고자한다.
any
any는 타입스크립트에서 가장 지양해야 할 타입이라고 한다. 그도 그럴것이 any는 말 그대로 모든 데이터 타입을 허용한다는 뜻이다.
한가지 흥미로운 점은 any[]로 추론된 배열은 push로 새로운 요소를 추가할때마다 추론하는 타입이 바뀐다는 점이다. 예를 들어, 빈 배열일때 1을 추가하면 number[]로 추론되고, 그 다음 'a'같은 문자열을 넣으면 (string | number)[] 로 추론된다는 것이다. 반면, pop을 때릴때는 이전 추론 단계로 돌아가지 못한다고 한다. 정확히는 한번 (string | number)[]와 같이 낙인(?)이 찍히면, 계속 그 타입을 유지한다는 것이다.
또한, any 타입을 통해 파생되는 결과물도 any 타입이 된다. 그렇기에 명시적으로 any를 반환하는 JSON.parse와 fetch 함수로 불러온 데이터같은 경우, 직접 타입을 타이핑하며 모든 타입이 any가 되는 것을 방지해야 한다.
interface User {
name: string;
age: number;
}
const jsonString = '{"name": "John", "age": 30}';
const user: User = JSON.parse(jsonString); // 명시적으로 User 타입 지정
console.log(user.name); // "John"
console.log(user.age); // 30
이러한 특성으로 인해 사실상 any 타입은 타입 검사를 포기한다는 선언과 같다고 한다.
이런 any를 사용할 일이 있기는 한데, 책을 보니 오버로딩을 할때 사용한다고 한다.
예시는 다음과 같다.
function add(x : string, y: string): string
function add(x : number, y: number): number
function add(x: any, y: any) {
return x + y;
}
굉장히 이상해보이는데 이처럼 x와 y의 타입케이스가 한가지 이상인 경우 이런식으로 함수 타입 선언을 중복하여 두가지 경우의 수를 가능하도록 할 수 있다고 한다. 그렇기에 any를 사용했지만 x와 y가 실제로 any가 되는 것이 아니라 오버로딩한 타입의 조합만 가능하다.
unknown
unknown은 any와 비슷하게 모든 타입을 대입할 수 있지만, 다른 점은 그 후 어떠한 동작도 수행할 수 없다는 것이다.
또 다른 말로는 unknown 타입인 데이터들을 가지고 뭘 연산하는 행위 자체가 에러로 처리된다는 것이다. 주로 try catch 문에서 많이 보게 되는데 이럴때 as로 타입을 주장하여 타입을 강제 지정하고는 한다.
try{
}
catch (e) {
const error = e as Error;
console.log(error.message);
}
void
세번째는 꽤나 친숙한 void이다. 자바스크립트를 포함한 다른 프로그래밍언어에도 있지만, 타입스크립트에서는 타입으로 사용되는 용도가 크다고 한다. 타입스크립트에서 void는 함수가 아무런 값을 반환하지 않음을 나타내기 위해 사용된다고 한다.
그런데 이걸 굳이 쓸데가 있나? 싶었는데 콜백함수를 타이핑할 때 사용될 수 있다고 한다.
예를 들어, forEach문을 통해 배열을 순회하며 각 요소를 console.log 때리는 로직이 있다고 하면, 이때 해당 함수의 반환 타입을 void로 지정함으로서 해당 함수는 반환값을 사용하지 않는 함수임을 명백히 할 수 있다는 것이다.
자바스크립트에서는 함수가 리턴 값이 없는 경우 기본적으로 undefined를 반환했지만, 타입스크립트에서는 이러한 의미없는 반환값이 처리될 여지(?)를 남기지 않기 위해 void가 생겼다고 이해했다.
function loopArray(arr: number[], callback: (item: number) => void): void {
arr.forEach(callback);
}
loopArray([1, 2, 3], (item) => {
console.log(item); // console.log의 반환값은 void
});
{}, object
처음 봤을때는 당연히 객체 타입을 의미하는줄 알았지만 {} 와 object는 타입스크립트에서는 null과 undefined를 제외한 모든 값을 의미한다. 이러한 광범위한 타입 추론 범위와 더불어 실제로 {} 타입인 변수를 사용하려고 하면 에러가 발생한다는 점으로 미루어보아 진짜 개쓸데없어 보였는데, 실제로도 쓸일은 거의 없다고 한다. 다만, 일부 활용될 여지(?)가 있을 수 있으니 알아둬서 나쁠 건 없다고 보인다.
never
마지막으로 never인데, 아직까지는 프로젝트중에서 써본적이 없어서 책을 보기 전까지는 이런 타입이 있는줄도 몰랐다.
never타입은 아무 값도 가질 수 없는 타입을 의미한다고 한다.
어떨때 쓰일 지 잘 감이 안왔는데 예를 들면, 다음과 같이 주어진 모든 값을 처리하는 스위치 케이스에서 주어진 값이 아닌 다른 값으로 인해 에러가 발생할 여지를 없애는 경우 사용될 수 있다고 한다.
type Animal = 'dog' | 'cat';
function getAnimalSound(animal: Animal): string {
switch (animal) {
case 'dog':
return 'Woof!';
case 'cat':
return 'Meow!';
default:
const _exhaustiveCheck: never = animal; // 이 코드는 실행되지 않는다
throw new Error(`Unhandled case: ${_exhaustiveCheck}`);
}
}
위 예시처럼 주어진 값은 개와 고양이가 전부고, never로 타입이 정해진 부분의 코드는 실행되지 않기에 Animal 타입에 주어진 값이 아닌 다른 값이 추가되는 경우를 방지할 수 있는 것이다.
undefined과 null의 차이
개인적으로 가장 궁금했던 것이 이 undefined과 null의 차이였는데, 그 이유는 자바스크립트에서는 undefined과 null이 별개의 값이기 때문이다.
그래서 타입스크립트에서는 이 둘을 어떻게 다룰지 궁금했다.
결론적으로 타입스크립트에서도 두 타입을 서로 다른 것으로 인식하지만, TS Config 메뉴에서 strictNullChecks를 체크 해제하면 undefined와 null을 같은 것으로 인식시킬 수 있다고 한다.
이처럼 이 둘을 딱히 구분하지 않는 사람들도 있다는 의미인 것 같다.
다만 대부분 undefined이 의도가 없는 빈값, null은 의도된 빈값으로 사용되는 경우가 많은 것 같으니 그렇게 알아놓는게 더 좋아보인다.