📗

2장 타입스크립트 기본 타입

2.1 기본타입 (Primitive Type)

타입스크립트가 자체적으로 제공하는 내장 타입을 살펴보겠습니다. 타입스크립트는 자바스크립트에 타입 안전장치를 추가한 확장판 언어인 셈이기 때문에, 기본적으로 자바스크립트에서 사용했던 number, string, boolean, null, undefined 등의 원시 타입은 당연히 제공됩니다.

2.1.1 타입 주석 (Type Annotation)

타입스크립트를 사용할 때는 변수의 이름 뒤에 콜론(:)과 함께 변수의 타입을 정의합니다. 이때 이 문법을 타입 주석 또는 타입 어노테이션이라고 부릅니다.
let str: string = "welcome!";
변수 선언 자체는 자바스크립트와 매우 유사하나, 타입 어노테이션을 통해 타입을 지정해 줍니다.
타입스크립트의 원시 타입을 빠르게 살펴보며 타입 주석에 익숙해져 봅시다.

2.1.2 number

let num1: number = 14; let num2: number = -14; let num3: number = 0.14; let num4: number = -0.14; let num5: number = Infinity; let num6: number = -Infinity; let num7: number = NaN;

2.1.3 string

let str1: string = "hello TypeScript"; let str2: string = "hello TypeScript"; let str3: string = `hello TypeScript`; let str4: string = `hello TypeScript ${num1}`;

2.1.4 boolean

let bool1: boolean = true; let bool2: boolean = false;

2.1.5 null

let null1: null = null;

2.1.6 undefined

let unde1: undefined = undefined;
 

2.2 독자적 타입

앞서 설명드린 원시 타입과 다르게 독자적 타입은 타입스크립트에만 존재합니다. 독자적 타입에는 any, unknown, void, never, enum, 리터럴 타입이 있습니다.

2.2.1 any

특정 변수의 타입을 정확히 모를 때 any 타입을 사용합니다. any 타입은 타입 검사를 전부 통과하기 때문에 사실상 타입 검사를 하지 않는다고 봐도 무방합니다.
let anyValue: any = 10; anyValue = "hello"; anyValue = true; anyValue = {}; anyValue = () => {};
이렇게 초기화한 값과 타입이 일치하지 않는 또 다른 값을 재할당하더라도 문제가 발생하지 않습니다.
anyValue.toUpperCase(); anyValue.toFixed(); let num: number = 10; num = anyValue
이후 이런 터무니없는 코드가 작성되더라도 문제는 없습니다. 하지만 예제 코드를 실행하게 될 경우 바로 anyValue에는 마지막에 함수를 할당했기 때문에 anyValue.toFixed()에서 런타임 에러가 발생합니다.
지금은 간단한 예제로 살펴보았기 때문에 문제가 발생하는 지점을 빠르게 확인할 수 있지만, 규모가 있는 프로젝트에서는 런타임 에러를 간단하게 처리하기 어려울 수 있습니다. 타입을 지정하여 안전한 개발 환경을 구축하고자 타입스크립트를 도입했다면, 굳이 any를 사용하여 타입스크립트의 이점을 포기할 필요가 없습니다.

2.2.2 unknown

unknown 타입 또한 변수의 타입을 정확하게 알지 못할 때 사용합니다. 하지만 any와는 차이점이 있습니다.
let num: number = 10; let unknownValue: unknown; unknownValue = ""; unknownValue = 1;
unknown으로 타입을 지정한 변수 unknownValue 역시 어떤 타입의 값을 재할당하여도 에러는 발생하지 않습니다. 하지만 unknown 타입의 값을 다른 타입의 변수에 할당하는 것은 불가능합니다.
num = unknownValue; // Error unknownValue.toUpperCase(); // Error
마지막 할당에서 unknownValue에 숫자 값이 담겼다고 해도 그 값을 num에 할당할 수는 없습니다. 또한 메서드 사용이나 연산도 불가능합니다. unknown 타입의 값을 신뢰하지 않는 것, 바로 이것이 any와의 차이점입니다.
💡
any type vs unknown type any 타입의 변수에는 어떤 타입의 값도 들어갈 수 있고, 어떤 타입의 변수에 any 타입의 값이 들어갈 수 있습니다. 반면에 unknown 타입의 변수에는 어떤 타입의 값도 들어갈 수 있지만, 어떤 타입의 변수에 unknown 타입의 값은 들어갈 수 없습니다. 가급적 unknown 타입의 사용을 권장합니다.

2.2.3 void

void는 공허함을 뜻하는 단어로, void 타입은 아무것도 없음을 의미하는 타입입니다.
function func1(): string { return "hi!"; } function func2(): void { console.log("hi!"); }
타입스크립트에서는 함수 반환 값에 대한 타입을 지정해야 합니다. 아래 함수처럼 반환 값이 없는 함수일 경우 반환 값 타입을 void라고 작성합니다.
변수의 타입으로도 void를 쓸 수 있습니다.
let abc: void; // abc = 1; // abc = ""; abc = undefined; // abc = null; // strictNullChecks 옵션을 끄면 null도 가능
void 타입에는 undefined 값만을 담을 수 있습니다. tsconfig의 strictNullChecks 옵션 여부에 따라 null도 담을 수 있지만 그 외의 타입 값은 담을 수 없습니다.

2.2.4 never

never는 존재하지 않는, 즉 불가능한 타입을 의미합니다.
function func5(): never { while (true) {} } function func6(): never { throw new Error(); }
두 함수는 모두 반환 값의 타입이 never 타입입니다. 그렇다면 두 함수의 공통점은 무엇일까요?
func5는 무한 루프를 도는 함수로, 함수가 끝나지 않아 반환 값을 반환할 수 없습니다. func6 역시 함수 실행 도중 에러를 발생시켜 함수가 강제적으로 종료됩니다. 이처럼 함수가 어떤 값을 반환하는 것 자체가 모순이고 절대 불가능할 때 never를 사용합니다.

2.2.5 enum 타입 (Enumerable Type)

enum 타입은 여러 가지 값들에 각각 이름을 부여해 열거해두고 사용하는 타입입니다.
const user1 = { name: "서주예", role: 0, // 0번은 관리자 }; const user2 = { name: "김소희", role: 1, // 1번은 일반 유저 }; const user3 = { name: "조병민", role: 2, // 2번은 게스트 };
예를 들어서 위와 같이 유저를 객체 단위로 표현한 코드가 있다고 생각해 보겠습니다. 유저 객체는 namerole이라는 프로퍼티를 가지고 있고, role 프로퍼티에는 숫자 데이터가 들어가 있지만, 우리는 이 숫자가 0은 관리자, 1은 일반 유저, 2는 게스트라는 것을 알고 있습니다.
하지만 코드가 길어지고, 혹은 다른 사람이 이 코드를 본다면 0, 1, 2가 각각 무엇을 의미하는지 단번에 파악하기 어려울 수 있습니다. 이럴 때 enum 타입을 사용합니다.
enum Role { ADMIN = 0, USER = 1, GUEST = 2, } const user1 = { name: "조병민", role: Role.ADMIN, }; const user2 = { name: "서주예", role: Role.USER, }; const user3 = { name: "김소희", role: Role.GUEST, };
enum Role을 선언해 0~2 값에 이름을 부여했습니다. 값을 작성하지 않아도 자동으로 0부터 1씩 증가하는 숫자 형태로 할당됩니다. 만약 10이라는 숫자부터 할당하고 싶더라도 10번이 될 이름에 10만 작성해두면 됩니다. 그다음엔 Role과 부여한 이름으로 값에 접근합니다.
enum 타입을 사용하니 확실히 앞서 작성했던 코드보다 의미가 명확해졌습니다. 타입스크립트의 타입 관련 문법들은 컴파일 되면서 사라지지만, enum은 컴파일이 되어도 사라지지 않고 자바스크립트의 객체로 변환된다는 특징도 있습니다.

2.2.6 리터럴 타입

리터럴 타입은 값으로 만든 타입입니다. 타입 안에 포함되는 값 중 하나를 타입인 것처럼 정의하여 사용합니다.
let numA: 10 = 10; let strA: "hello" = "hello"; let boolA: true = true;
2.1장에서 봤던 예시 코드와 매우 유사한 것처럼 보이지만 타입 선언 부분에서 차이가 있습니다. 콜론(:) 뒤에 number, string, boolean과 같은 타입이 아니라 값을 직접 선언해 준 것입니다. 값처럼 보이는 10, “hello”, true는 값 그 자체를 타입으로 갖게 합니다.
numA = 20; // Error
그렇기 때문에 처음 지정해 주었던 값이 아닌 다른 값을 재할당한다면 에러가 발생하게 됩니다.
 

2.3 기본적인 타입 조합

타입스크립트에선 타입을 조합해서 사용할 수 있습니다. 여러 개의 타입을 합성해서 새롭게 만들어낸 타입을 대수 타입이라고 하며, 대수 타입으로는 유니온 타입, 인터섹션 타입, 서로소 유니온 타입, 템플릿 리터럴 타입 등이 있습니다. 이번 장에서는 먼저 유니온 타입, 템플릿 리터럴 타입에 대해 다뤄보겠습니다.

2.3.1 유니온 타입

유니온 타입은 합집합 타입을 의미합니다.
let a: string | number | boolean; a = 1; a = "hello"; a = true;
유니온 타입은 | 기호를 사용하고, 이 타입 집합에 속하는 어떤 값이든 할당할 수 있습니다.
합집합 타입이 있듯이 교집합 타입도 물론 존재합니다.
let variable: number & string;
다만 기본 타입에서는 서로 공유하거나 겹치는 값이 존재하지 않으므로 무조건 never 타입으로 추론됩니다. 객체를 공부한 뒤에 더 자세히 다뤄보겠습니다.

2.3.2 템플릿 리터럴 타입

문자열 리터럴 타입을 기반으로 특정 패턴을 갖는 문자열 타입들을 만드는 기능입니다.
type Animal = "dog" | "cat" | "lion"; type Color = "white" | "yellow" | "green"; type AnimalandColor = `${Animal}-${Color}`;
 

2.4 타입 계층 이해하기

2.4.1 타입을 집합으로 바라보기

앞서 살펴본 바와 같이 타입을 다룰 때는 여러 타입을 합쳐 합집합 타입을 만들기도 하고, 공유할 수 있는 프로퍼티가 있다면 교집합 타입을 만들기도 합니다. 이렇게 하나의 타입은 동일한 프로퍼티와 특징을 갖는 여러 개의 값을 모아둔 집합으로 볼 수 있습니다.
let num: 20 = 20; // type : 20(Number Literal Type)
20이라는 숫자형 리터럴 타입의 경우에도 20만을 포함하는 아주 작은 단위의 집합입니다. 하지만 다르게 생각해 보면 20 타입의 20이라는 값은 기본 타입인 number 타입에 속하기도 합니다. 즉, 20 숫자형 리터럴 타입은 number 타입의 부분집합이라고 할 수 있습니다. 여기서 부모 집합이 되는 숫자형 타입을 슈퍼 타입(부모 타입), 자식이 되는 숫자형 리터럴 타입을 서브 타입(자식 타입)이라고 부르기도 합니다.
 
타입 계층도타입 계층도
타입 계층도
2장에서 배웠던 기본 타입과 독자적 타입을 제외하고도 타입스크립트에는 다양한 타입들이 존재하고, 모든 타입들은 위의 계층형 구조로 표현할 수 있습니다.

2.4.2 타입 호환성

타입 호환성은 어떤 타입을 다른 타입으로 취급해도 괜찮은지 판단하는 것을 뜻합니다. number 타입은 슈퍼 타입이며 숫자형 리터럴 타입은 서브 타입이므로 이런 관계도로 표현할 수 있습니다.
타입 호환성타입 호환성
타입 호환성
특정 숫자형 리터럴 타입에 속하는 모든 값은 number 타입으로 취급해도 안전합니다. 이렇게 서브 타입의 값을 슈퍼 타입으로 취급하는 것을 업 캐스팅이라고 합니다. 슈퍼 타입의 값을 서브 타입으로 취급하는 것은 다운 캐스팅입니다. 하지만 위 예시와 마찬가지로 대부분의 상황에서 불가능한 경우가 많습니다.
let num1: number = 10; let num2: 10 = 10; // 업 캐스팅 가능 num1 = num2; // 다운 캐스팅 불가능 num2 = num1; // Error

2.4.3 any 타입과 unknown 타입

2.2.2에서 any 타입과 unknown 타입의 차이를 설명드렸습니다. 둘 다 다른 타입의 값을 할당받을 수 있다는 점은 동일하지만, 다른 타입의 변수에 any 타입의 값을 할당하는 것은 가능한 반면 unknown 타입의 값은 불가능하다는 점에 차이가 있었습니다. 이 차이점 역시 타입 계층도를 통해 살펴보면 더 쉽게 이해할 수 있습니다.
let anyValue: any; anyValue = 15; anyValue = () => {}; anyValue = [];
위의 코드에서는 any 타입에 각각 number, 함수, 배열 타입의 값을 할당하고 있습니다. 해당 타입들을 계층도에서 보면 any 타입보다 number, 함수, 배열 타입이 하위에 위치하고 있다는 것을 알 수 있습니다. 각 타입들이 상위 계층 타입인 any 타입 변수에 업 캐스팅되었다고 설명할 수 있습니다.
기본적으로 타입스크립트에서는 타입의 업 캐스팅은 자유롭게 허용하지만, 다운 캐스팅은 허용하지 않습니다. unknown 타입을 계층도에서 보면 모든 타입의 상위 계층 타입입니다. 그렇기 때문에 어떤 타입의 값도 unknown 타입에 할당할 수 있습니다.
let unknownValue: unknown; unknownValue = 15; unknownValue = () => {}; unknownValue = [];
그렇다면 반대로, 다운 캐스팅의 예시를 보겠습니다.
let numValue: number; let unknownValue: unknown; numValue = unknownValue; // Error
위의 코드에서 unknown 타입의 변수를 number 타입의 변수에 할당하려고 합니다. unknown 타입은 계층도에서 모든 타입의 가장 상위 타입이기 때문에 상위 계층 타입을 하위 계층 타입에 할당하려고 하는, 즉 “다운 캐스팅”이므로 에러가 발생합니다.
그렇다면 여기서 의문이 생깁니다. any 타입의 값은 어떻게 다른 타입에 할당될 수 있었을까요?
let numValue: number; let anyValue: any; numValue = anyValue;
any 타입인 상위 계층 타입을 number 타입인 하위 계층 타입에 할당하려고 하는 것은 다운 캐스팅이라고 할 수 있습니다. 원칙대로라면 에러가 발생해야 하지만 실제 코드 상에서는 에러가 발생하지 않습니다.
any 타입은 논리적인 다운 캐스팅을 하더라도 에러가 발생하지 않습니다. any는 계층도에서 unknown 아래, 다른 타입들 위에 위치하지만 사실상 계층도에서 벗어나 어디에나 위치하더라도 상관없습니다.
💡
더 알아가기 아무리 any 타입이라 할지라도 never 타입에 다운 캐스팅은 불가능합니다. never 타입은 모든 타입의 제일 하위 계층이기 때문입니다.
이처럼 타입을 계층 구조로 바라본다면, 타입스크립트의 타입에 대한 이해도를 올릴 수 있습니다.