목록으로

프로그래밍 · TypeScript

TypeScript 완전정복 9화: 유니언 타입과 리터럴 타입 이해하기

BeanCon
TypeScript 유니언 타입과 리터럴 타입의 차이와 활용법을 설명하는 대표 이미지

TypeScript 완전정복 9화입니다. 하나의 변수에 여러 타입 가능성을 허용하는 유니언 타입과 정확히 지정된 값만 허용하는 리터럴 타입을 입문자 눈높이에서 정리했습니다. | 기호, typeof 타입 좁히기, 공통 기능, 함수 매개변수와 반환값, 배열과 유니언 타입, 타입 별칭, 문자열/숫자/불리언 리터럴 타입, 상태값과 옵션 제한, as const, 실무 설계 기준까지 다룹니다.

목차

유니언 타입과 리터럴 타입 완벽 이해하기

지난 시간에는 여러 값을 하나의 변수로 관리하는 배열과, 위치별 타입이 정해진 튜플을 살펴봤습니다.

const scores: number[] = [
  90,
  85,
  100,
];

const userInfo: [string, number] = [
  "김타입",
  25,
];

그런데 실제 프로그램에서는 하나의 변수에 항상 한 종류의 값만 들어오는 것은 아닙니다.

사용자 ID가 숫자일 수도 있고 문자열일 수도 있습니다.

let userId: number | string;

서버 요청 상태가 다음 중 하나일 수도 있습니다.

대기 중
불러오는 중
성공
실패

아무 문자열이나 허용하는 대신 정해진 문자열만 저장하고 싶다면 다음처럼 작성할 수 있습니다.

let requestStatus:
  | "idle"
  | "loading"
  | "success"
  | "error";

TypeScript는 여러 가능성을 허용하면서도 아무 값이나 들어오는 것은 막을 수 있습니다.

오늘 만나볼 두 주인공은 다음과 같습니다.

유니언 타입
여러 타입 중 하나를 허용한다.

리터럴 타입
정확히 지정된 값만 허용한다.

유니언 타입이 여러 출입구를 만들어주는 기술이라면, 리터럴 타입은 출입 명단을 들고 서 있는 보안요원입니다. 🛂

문은 여러 개지만 명단에 없는 값은 들어올 수 없습니다.

1. 이번 시간에 배울 내용

이번 시간에는 다음 내용을 알아봅니다.

  • 유니언 타입이란 무엇인가?
  • `|` 기호는 어떤 의미인가?
  • 문자열과 숫자를 함께 허용하는 방법
  • 유니언 타입의 값을 바로 사용할 수 없는 이유
  • `typeof`로 실제 타입을 확인하는 방법
  • 공통으로 사용할 수 있는 기능은 무엇인가?
  • 유니언 타입을 함수 매개변수와 반환값에 적용하는 방법
  • 배열과 유니언 타입을 함께 사용하는 방법
  • 리터럴 타입이란 무엇인가?
  • 문자열, 숫자, 불리언 리터럴 타입
  • `let`과 `const`의 리터럴 타입 추론 차이
  • 리터럴 타입과 유니언 타입을 조합하는 방법
  • 상태값과 옵션을 안전하게 제한하는 방법
  • 타입 별칭으로 복잡한 타입을 정리하는 방법
  • 실무에서 유니언 타입을 설계하는 기준

이번 편의 핵심 문장은 다음과 같습니다.

가능성은 열어두되, 허용되지 않은 값은 문 앞에서 돌려보낸다.

2. 유니언 타입이란?

유니언 타입(Union Type)은 하나의 값이 여러 타입 중 하나가 될 수 있도록 허용하는 타입입니다.

다음 변수를 살펴보겠습니다.

let userId: string | number;

`|` 기호는 또는이라는 의미입니다.

string | number
문자열 또는 숫자

따라서 `userId`에는 문자열을 저장할 수 있습니다.

userId = "USER-100";

숫자도 저장할 수 있습니다.

userId = 100;

하지만 불리언은 허용되지 않습니다.

userId = true;

오류 메시지는 다음과 비슷합니다.

Type 'boolean' is not assignable
to type 'string | number'.

TypeScript의 반응을 자연스럽게 번역하면 다음과 같습니다.

“문자열과 숫자는 초대했지만 불리언에게는 초대장을 보낸 적이 없습니다.”

유니언 타입은 여러 타입의 연합입니다.

string | number

문자열 왕국과 숫자 왕국이 하나의 통행증을 공동 발급한 셈입니다.

3. 왜 유니언 타입이 필요할까?

사용자 ID를 숫자로만 관리하는 프로그램도 있습니다.

const userId: number = 100;

하지만 외부 시스템과 연동하거나 새로운 ID 체계를 적용하면 문자열이 사용될 수 있습니다.

const userId: string = "USER-100";

함수가 두 종류의 ID를 모두 처리해야 한다면 어떻게 해야 할까요?

잘못된 방법은 `any`를 사용하는 것입니다.

function printUserId(id: any) {
  console.log(id);
}

`any`는 문자열과 숫자뿐 아니라 거의 모든 값을 허용합니다.

printUserId("USER-100");
printUserId(100);
printUserId(true);
printUserId({});

우리가 원하는 것은 문자열과 숫자만 허용하는 것입니다.

유니언 타입을 사용하면 정확하게 제한할 수 있습니다.

function printUserId(
  id: string | number
) {
  console.log(id);
}

이제 문자열과 숫자는 전달할 수 있습니다.

printUserId("USER-100");
printUserId(100);

불리언은 전달할 수 없습니다.

printUserId(true);

유니언 타입은 `any`처럼 모든 문을 활짝 열지 않습니다.

필요한 문만 골라 열어두는 스마트 도어락입니다. 🔐

4. 두 개보다 많은 타입도 연결할 수 있다

유니언 타입은 두 개의 타입만 연결하는 문법이 아닙니다.

세 개 이상의 타입도 연결할 수 있습니다.

let value:
  | string
  | number
  | boolean;

다음 값은 모두 저장할 수 있습니다.

value = "TypeScript";
value = 100;
value = true;

하지만 배열은 허용되지 않습니다.

value = [1, 2, 3];

`null`과 `undefined`도 함께 사용할 수 있습니다.

let userName:
  | string
  | null
  | undefined;

다음 값들을 저장할 수 있습니다.

userName = "김타입";
userName = null;
userName = undefined;

지난 7편에서 사용했던 다음 타입도 유니언 타입입니다.

string | null

우리는 이미 유니언 타입을 사용하고 있었습니다.

다만 그때는 이름표를 자세히 보지 않았을 뿐입니다.

5. 유니언 타입 작성 방법

유니언 타입은 각 타입 사이에 `|` 기호를 작성합니다.

타입1 | 타입2

예를 들어 문자열 또는 숫자를 허용하려면 다음처럼 작성합니다.

string | number

문자열, 숫자 또는 불리언을 허용하려면 다음과 같습니다.

string | number | boolean

타입이 많아지면 여러 줄로 작성할 수 있습니다.

let result:
  | string
  | number
  | null;

또는 타입 별칭을 사용해 이름을 붙일 수 있습니다.

type Result =
  | string
  | number
  | null;

이제 다음처럼 사용할 수 있습니다.

let result: Result;

복잡한 타입에 이름을 붙이면 코드의 의미가 더 잘 드러납니다.

6. 유니언 타입의 값은 왜 바로 사용할 수 없을까?

다음 함수를 살펴보겠습니다.

function printValue(
  value: string | number
) {
  console.log(value.toUpperCase());
}

오류가 발생합니다.

`toUpperCase()`는 문자열에서 사용할 수 있는 메서드입니다.

"typescript".toUpperCase();

하지만 숫자에는 존재하지 않습니다.

100.toUpperCase();

함수의 `value`에는 문자열이 들어올 수도 있지만 숫자가 들어올 수도 있습니다.

TypeScript는 어떤 타입이 들어왔는지 아직 확신할 수 없습니다.

따라서 문자열 전용 메서드 사용을 허용하지 않습니다.

TypeScript는 다음 상황을 걱정하고 있습니다.

printValue(100);

함수 내부에서 다음 코드가 실행되면 문제가 발생합니다.

100.toUpperCase();

숫자에게 대문자로 변신하라고 요구한 것입니다.

숫자는 알파벳이 아니기 때문에 상당히 난처합니다.

7. 유니언 타입에서는 공통 기능만 바로 사용할 수 있다

유니언 타입의 값에는 모든 구성 타입에서 공통으로 사용할 수 있는 기능만 바로 사용할 수 있습니다.

function printValue(
  value: string | number
) {
  console.log(value.toString());
}

`toString()`은 문자열과 숫자에서 모두 사용할 수 있습니다.

따라서 오류가 발생하지 않습니다.

다음 속성은 사용할 수 없습니다.

value.length;

문자열에는 `length`가 있지만 숫자에는 없습니다.

다음 메서드도 사용할 수 없습니다.

value.toFixed(2);

`toFixed()`는 숫자에서만 사용할 수 있습니다.

TypeScript의 판단 기준은 다음과 같습니다.

string에서 사용 가능?
number에서도 사용 가능?
둘 다 가능하면 허용
하나라도 불가능하면 제한

여러 사람이 공동으로 사용하는 회의실에서는 모두가 사용할 수 있는 장비만 기본 제공되는 것과 같습니다.

문자열 전용 노래방 기계와 숫자 전용 계산기는 각자의 방으로 이동한 뒤 사용해야 합니다.

8. `typeof`로 타입 확인하기

유니언 타입의 값을 안전하게 사용하려면 현재 값이 어떤 타입인지 확인해야 합니다.

이때 `typeof` 연산자를 사용할 수 있습니다.

function printValue(
  value: string | number
) {
  if (typeof value === "string") {
    console.log(
      value.toUpperCase()
    );
  }
}

조건문 안에서 TypeScript는 `value`가 문자열이라는 사실을 이해합니다.

조건문 밖
string | number

조건문 안
string

숫자인 경우도 처리할 수 있습니다.

function printValue(
  value: string | number
) {
  if (typeof value === "string") {
    console.log(
      value.toUpperCase()
    );
  } else {
    console.log(
      value.toFixed(2)
    );
  }
}

함수를 호출해 보겠습니다.

printValue("typescript");
printValue(100);

출력 결과:

TYPESCRIPT
100.00

이렇게 조건을 통해 타입의 범위를 줄이는 것을 타입 좁히기(Type Narrowing)라고 합니다.

타입 좁히기는 PART 4에서 더욱 깊게 다룰 예정입니다.

이번 시간에는 유니언 타입의 값을 사용할 때 실제 타입을 먼저 확인해야 한다는 점만 기억하면 충분합니다.

9. `typeof`가 반환하는 문자열

기본 타입을 `typeof`로 검사할 때는 타입 이름을 문자열로 작성합니다.

typeof value === "string"

숫자는 다음과 같습니다.

typeof value === "number"

불리언은 다음과 같습니다.

typeof value === "boolean"

함수는 다음과 같습니다.

typeof value === "function"

주의할 점은 다음처럼 작성하지 않는 것입니다.

typeof value === string

`string`은 여기에서 문자열 값이 아닙니다.

올바른 코드는 큰따옴표나 작은따옴표가 포함된 `"string"`입니다.

typeof value === "string"

`typeof` 경비원에게는 타입 이름을 문자열로 말해야 합니다.

“문자열입니다”라고 대답해야지, 타입 자체를 신분증처럼 던지면 대화가 되지 않습니다.

10. 함수 매개변수에 유니언 타입 사용하기

문자열과 숫자를 모두 출력하는 함수를 만들어 보겠습니다.

function printId(
  id: string | number
): void {
  console.log(`ID: ${id}`);
}

문자열을 전달할 수 있습니다.

printId("USER-100");

숫자도 전달할 수 있습니다.

printId(100);

불리언은 전달할 수 없습니다.

printId(true);

함수 내부에서 타입별로 다른 동작을 수행할 수도 있습니다.

function formatId(
  id: string | number
): string {
  if (typeof id === "number") {
    return `NUMBER-${id}`;
  }

  return id.toUpperCase();
}

호출 결과:

console.log(formatId(100));
console.log(formatId("user-100"));

출력 결과:

NUMBER-100
USER-100

11. 유니언 타입을 반환하는 함수

함수의 반환 타입에도 유니언 타입을 사용할 수 있습니다.

다음 함수는 숫자를 반환할 수도 있고 오류 메시지를 반환할 수도 있습니다.

function divide(
  firstNumber: number,
  secondNumber: number
): number | string {
  if (secondNumber === 0) {
    return "0으로 나눌 수 없습니다.";
  }

  return firstNumber / secondNumber;
}

정상적인 계산:

const firstResult = divide(10, 2);

결과:

5

0으로 나누는 경우:

const secondResult = divide(10, 0);

결과:

0으로 나눌 수 없습니다.

반환된 값을 사용할 때는 타입을 확인해야 합니다.

const result = divide(10, 2);

if (typeof result === "number") {
  console.log(
    result.toFixed(2)
  );
} else {
  console.log(result);
}

다만 실무에서는 성공 결과와 실패 결과를 단순히 숫자와 문자열로 섞기보다 명확한 객체 구조로 설계하기도 합니다.

객체와 사용자 정의 타입은 PART 3에서 자세히 다룰 예정입니다.

12. 배열과 유니언 타입 함께 사용하기

숫자와 문자열을 함께 저장하는 배열을 만들 수 있습니다.

const values:
  (string | number)[] = [
    "TypeScript",
    100,
    "JavaScript",
    200,
  ];

괄호가 중요합니다.

(string | number)[]

의미는 다음과 같습니다.

문자열 또는 숫자를 요소로 가지는 배열

문자열과 숫자를 추가할 수 있습니다.

values.push("Python");
values.push(300);

불리언은 추가할 수 없습니다.

values.push(true);

배열을 순회할 때 각 요소의 타입을 확인해야 합니다.

values.forEach((value) => {
  if (typeof value === "string") {
    console.log(
      value.toUpperCase()
    );
  } else {
    console.log(
      value.toFixed(2)
    );
  }
});

13. 괄호 위치에 따른 의미 차이

다음 두 타입은 의미가 다릅니다.

(string | number)[]
string | number[]

첫 번째 타입은 문자열 또는 숫자를 요소로 가지는 배열입니다.

const firstValue:
  (string | number)[] = [
    "A",
    1,
    "B",
    2,
  ];

두 번째 타입은 문자열 하나 또는 숫자 배열을 의미합니다.

let secondValue:
  string | number[];

문자열을 저장할 수 있습니다.

secondValue = "TypeScript";

숫자 배열도 저장할 수 있습니다.

secondValue = [1, 2, 3];

하지만 문자열 배열은 저장할 수 없습니다.

secondValue = [
  "JavaScript",
  "TypeScript",
];

괄호 하나가 데이터의 세계관을 바꿉니다.

(string | number)[]
배열 안에서 타입이 섞일 수 있음

string | number[]
값 자체가 문자열이거나 숫자 배열

TypeScript에서 괄호는 장식이 아니라 교통 표지판입니다.

14. 유니언 타입 배열과 튜플의 차이

다음은 유니언 타입 배열입니다.

const userInfo:
  (string | number)[] = [
    "김타입",
    25,
  ];

문자열과 숫자를 원하는 순서로 추가할 수 있습니다.

userInfo.push(30);
userInfo.push("서울");

다음은 튜플입니다.

const userInfo:
  [string, number] = [
    "김타입",
    25,
  ];

첫 번째 요소는 문자열, 두 번째 요소는 숫자로 정해져 있습니다.

정리하면 다음과 같습니다.

유니언 타입 배열
각 요소가 여러 타입 중 하나일 수 있음
위치별 의미는 고정되지 않음

튜플
각 위치의 타입이 고정됨
순서 자체가 데이터 구조의 일부

다음 데이터는 목록이므로 유니언 타입 배열이 어울릴 수 있습니다.

const searchKeywords:
  (string | number)[] = [
    "TypeScript",
    2026,
  ];

다음 데이터는 위치별 의미가 있으므로 튜플이 적합합니다.

const product:
  [string, number] = [
    "키보드",
    120000,
  ];

15. 객체 속성에 유니언 타입 사용하기

객체 속성에도 유니언 타입을 사용할 수 있습니다.

type User = {
  id: string | number;
  name: string;
};

숫자 ID를 사용할 수 있습니다.

const firstUser: User = {
  id: 100,
  name: "김타입",
};

문자열 ID도 사용할 수 있습니다.

const secondUser: User = {
  id: "USER-200",
  name: "이컴파일",
};

불리언 ID는 허용되지 않습니다.

const thirdUser: User = {
  id: true,
  name: "박유니언",
};

객체 타입은 PART 3에서 본격적으로 다루지만, 유니언 타입은 변수뿐 아니라 객체 속성에도 적용할 수 있다는 점을 기억해 두세요.

16. 타입 별칭으로 유니언 타입 정리하기

같은 유니언 타입을 여러 곳에서 반복하면 코드가 길어집니다.

let userId:
  string | number;

function printUserId(
  id: string | number
) {
  console.log(id);
}

function findUser(
  id: string | number
) {
  console.log(id);
}

타입 별칭을 사용해 이름을 붙일 수 있습니다.

type UserId =
  string | number;

이제 다음처럼 사용할 수 있습니다.

let userId: UserId;

function printUserId(
  id: UserId
) {
  console.log(id);
}

function findUser(
  id: UserId
) {
  console.log(id);
}

타입 별칭은 단순히 코드를 줄이는 기능만 있는 것이 아닙니다.

`string | number`보다 `UserId`가 더 구체적인 의미를 전달합니다.

string | number
타입의 모양만 알려줌

UserId
이 값이 사용자 ID라는 의미까지 알려줌

타입에 이름을 붙이는 것은 데이터에게 직업 명찰을 달아주는 일입니다.

17. 유니언 타입을 너무 넓게 만들면 생기는 문제

다음 타입을 살펴보겠습니다.

type Value =
  | string
  | number
  | boolean
  | null
  | undefined;

거의 모든 기본값을 받을 수 있습니다.

처음에는 편리해 보입니다.

let value: Value;

하지만 값을 사용할 때마다 여러 가능성을 확인해야 합니다.

if (typeof value === "string") {
  // 문자열 처리
} else if (typeof value === "number") {
  // 숫자 처리
} else if (typeof value === "boolean") {
  // 불리언 처리
} else {
  // null 또는 undefined 처리
}

유니언 타입은 필요한 가능성만 포함해야 합니다.

type UserId =
  string | number;
type SelectedId =
  number | null;
type SearchKeyword =
  string | undefined;

유니언 타입을 무작정 넓히면 타입 안전성이 사라지는 것은 아니지만, 사용하는 코드가 복잡해집니다.

손님을 많이 초대할수록 현관은 북적이고 신분 확인 줄은 길어집니다.

18. 리터럴 타입이란?

지금까지 `string`은 모든 문자열을 허용했습니다.

let message: string;

다음 문자열을 모두 저장할 수 있습니다.

message = "안녕하세요.";
message = "TypeScript";
message = "아무 문자열";

하지만 특정 문자열 하나만 허용하고 싶을 수 있습니다.

let direction: "left";

이 변수에는 오직 `"left"`만 저장할 수 있습니다.

direction = "left";

다른 문자열은 허용되지 않습니다.

direction = "right";

`"left"`처럼 특정 값 자체를 타입으로 사용하는 것을 리터럴 타입(Literal Type)이라고 합니다.

string
모든 문자열을 허용

"left"
정확히 "left"만 허용

`string`이 모든 문자열이 모인 대륙이라면, `"left"`는 지도에 핀 하나를 꽂은 정확한 주소입니다. 📍

19. 문자열 리터럴 타입

문자열 값을 타입으로 사용할 수 있습니다.

let status: "success";

다음 값만 저장할 수 있습니다.

status = "success";

다른 문자열은 오류입니다.

status = "error";
status = "loading";

문자열 리터럴 타입 하나만 사용하면 활용 범위가 좁습니다.

하지만 유니언 타입과 결합하면 강력해집니다.

let status:
  | "idle"
  | "loading"
  | "success"
  | "error";

이제 네 가지 값 중 하나를 저장할 수 있습니다.

status = "idle";
status = "loading";
status = "success";
status = "error";

목록에 없는 값은 저장할 수 없습니다.

status = "complete";

20. 문자열 리터럴 유니언은 선택지를 만든다

리터럴 타입과 유니언 타입을 조합하면 허용 가능한 문자열 목록을 만들 수 있습니다.

type Direction =
  | "left"
  | "right"
  | "up"
  | "down";

함수의 매개변수로 사용할 수 있습니다.

function move(
  direction: Direction
) {
  console.log(
    `${direction} 방향으로 이동합니다.`
  );
}

올바른 호출:

move("left");
move("right");
move("up");
move("down");

잘못된 호출:

move("forward");

TypeScript는 오타도 잡아낼 수 있습니다.

move("rigth");

`"right"`를 `"rigth"`로 잘못 작성했습니다.

일반 `string` 타입이었다면 이 오타를 발견하지 못합니다.

function move(
  direction: string
) {
  console.log(direction);
}

리터럴 타입은 자동완성과 오타 방지 기능까지 제공합니다.

개발자가 방향을 잘못 입력하면 캐릭터가 절벽으로 달려가기 전에 TypeScript가 지도부터 압수합니다. 🗺️

21. 숫자 리터럴 타입

숫자도 특정 값 자체를 타입으로 사용할 수 있습니다.

let diceNumber: 1 | 2 | 3 | 4 | 5 | 6;

주사위에서 나올 수 있는 숫자만 허용합니다.

diceNumber = 1;
diceNumber = 6;

범위를 벗어난 값은 허용하지 않습니다.

diceNumber = 7;

별점 타입도 만들 수 있습니다.

type Rating =
  | 1
  | 2
  | 3
  | 4
  | 5;

함수에 적용합니다.

function submitRating(
  rating: Rating
) {
  console.log(
    `${rating}점을 등록했습니다.`
  );
}

정상 호출:

submitRating(5);

오류가 발생하는 호출:

submitRating(10);

숫자 리터럴 타입은 허용 범위가 작고 명확할 때 유용합니다.

22. 불리언 리터럴 타입

불리언에도 리터럴 타입이 있습니다.

let successOnly: true;

이 변수에는 `true`만 저장할 수 있습니다.

successOnly = true;

`false`는 저장할 수 없습니다.

successOnly = false;

반대로 `false`만 허용할 수도 있습니다.

let failureOnly: false;

불리언 리터럴 타입을 단독으로 자주 사용하는 것은 아니지만, 객체의 상태를 정교하게 표현할 때 활용할 수 있습니다.

type SuccessResult = {
  success: true;
  message: string;
};
type FailureResult = {
  success: false;
  error: string;
};

이런 구조는 PART 3과 PART 4에서 더욱 자세히 다룹니다.

23. `let`과 `const`의 리터럴 타입 추론

다음 코드를 비교해 보겠습니다.

let firstStatus = "loading";
const secondStatus = "loading";

`let` 변수는 나중에 다른 문자열로 바뀔 수 있습니다.

firstStatus = "success";

따라서 TypeScript는 `firstStatus`를 일반 `string`으로 추론합니다.

let firstStatus: string;

반면 `const`는 다른 값을 다시 할당할 수 없습니다.

secondStatus = "success";

오류가 발생합니다.

따라서 TypeScript는 더 구체적인 리터럴 타입으로 추론합니다.

const secondStatus: "loading";

정리하면 다음과 같습니다.

let status = "loading"
추론 타입: string

const status = "loading"
추론 타입: "loading"

`let`은 미래에 변신할 가능성이 있으므로 넓은 여권을 발급받습니다.

`const`는 평생 `"loading"`으로 살기 때문에 이름과 직업이 모두 박힌 고정 신분증을 받습니다.

24. `let`에서도 리터럴 타입을 지정할 수 있다

`let` 변수라도 리터럴 유니언 타입을 직접 명시하면 허용된 값 안에서 변경할 수 있습니다.

let status:
  | "idle"
  | "loading"
  | "success"
  | "error" = "idle";

다른 허용 값으로 변경할 수 있습니다.

status = "loading";
status = "success";

허용되지 않은 값은 저장할 수 없습니다.

status = "complete";

이 패턴은 UI 상태를 관리할 때 자주 사용합니다.

idle
아무 작업도 하지 않는 상태

loading
데이터를 불러오는 상태

success
성공한 상태

error
오류가 발생한 상태

25. 타입 별칭으로 리터럴 타입 정리하기

상태 타입을 여러 곳에서 사용한다면 타입 별칭을 만드는 것이 좋습니다.

type RequestStatus =
  | "idle"
  | "loading"
  | "success"
  | "error";

변수에 적용합니다.

let status: RequestStatus =
  "idle";

함수 매개변수에도 적용합니다.

function updateStatus(
  status: RequestStatus
) {
  console.log(
    `현재 상태: ${status}`
  );
}

올바른 호출:

updateStatus("loading");

잘못된 호출:

updateStatus("complete");

반환 타입에도 사용할 수 있습니다.

function getStatus():
  RequestStatus {
  return "success";
}

타입 이름을 만들어 두면 상태 목록을 한곳에서 관리할 수 있습니다.

26. 일반 문자열 매개변수의 문제

버튼의 크기를 지정하는 함수를 만들어 보겠습니다.

function createButton(
  size: string
) {
  console.log(
    `${size} 버튼을 생성합니다.`
  );
}

정상적인 값:

createButton("small");
createButton("medium");
createButton("large");

하지만 이상한 값도 전달할 수 있습니다.

createButton("gigantic");
createButton("banana");
createButton("오늘은 버튼이 싫어요");

문법적으로는 모두 문자열입니다.

TypeScript는 어떤 문자열이 올바른 옵션인지 알 수 없습니다.

리터럴 유니언 타입으로 제한해 보겠습니다.

type ButtonSize =
  | "small"
  | "medium"
  | "large";
function createButton(
  size: ButtonSize
) {
  console.log(
    `${size} 버튼을 생성합니다.`
  );
}

이제 허용된 값만 전달할 수 있습니다.

createButton("small");
createButton("medium");
createButton("large");

바나나 크기의 버튼은 생성할 수 없습니다. 🍌

27. 문자열 리터럴 타입과 자동완성

다음 함수를 작성했다고 생각해 보겠습니다.

type Theme =
  | "light"
  | "dark"
  | "system";

function setTheme(
  theme: Theme
) {
  console.log(theme);
}

VS Code에서 다음 코드를 작성하면 허용 가능한 값이 자동완성 후보로 나타납니다.

setTheme(
  // "light"
  // "dark"
  // "system"
);

일반 `string` 타입에서는 가능한 값을 개발자가 기억해야 합니다.

function setTheme(
  theme: string
) {}

리터럴 타입을 사용하면 타입 자체가 문서 역할을 합니다.

type Theme =
  | "light"
  | "dark"
  | "system";

별도의 설명서를 펼치지 않아도 허용 가능한 옵션을 바로 확인할 수 있습니다.

28. 오타로 인한 버그 방지하기

상태값을 일반 문자열로 관리해 보겠습니다.

let status: string =
  "loading";

다음 코드에는 오타가 있습니다.

status = "sucess";

`"success"`에서 `c` 하나가 빠졌습니다.

하지만 타입은 문자열이므로 TypeScript는 오류를 알려주지 않습니다.

리터럴 타입을 사용하면 오타를 발견할 수 있습니다.

type Status =
  | "loading"
  | "success"
  | "error";
let status: Status =
  "loading";

status = "sucess";

TypeScript가 오류를 알려줍니다.

Type '"sucess"' is not assignable
to type 'Status'.

오타 한 글자가 며칠짜리 버그 여행을 떠나기 전에 TypeScript가 공항에서 붙잡습니다. ✈️

29. 유니언 타입과 리터럴 타입 함께 사용하기

유니언 타입에는 일반 타입과 리터럴 타입을 함께 넣을 수도 있습니다.

type Port =
  | 80
  | 443
  | number;

하지만 `number`를 포함하면 모든 숫자를 허용하므로 `80 | 443`의 제한 의미가 사라집니다.

let port: Port;

port = 80;
port = 443;
port = 12345;

문자열에서도 마찬가지입니다.

type Status =
  | "loading"
  | "success"
  | string;

`string`이 포함되어 있으므로 모든 문자열을 허용합니다.

let status: Status;

status = "loading";
status = "banana";

리터럴 타입으로 값을 제한하려면 넓은 타입을 함께 넣지 않도록 주의해야 합니다.

type Status =
  | "loading"
  | "success"
  | "error";

출입 명단을 작성해 놓고 마지막 줄에 “모든 사람 입장 가능”이라고 쓰면 명단의 의미가 사라집니다.

30. 숫자와 특정 문자열을 함께 허용하기

실무에서는 숫자 또는 특별한 문자열을 허용하는 경우가 있습니다.

예를 들어 너비를 숫자 또는 `"auto"`로 표현할 수 있습니다.

type Width =
  | number
  | "auto";

다음 값은 허용됩니다.

let width: Width;

width = 300;
width = "auto";

다음 문자열은 허용되지 않습니다.

width = "wide";

함수에서 타입을 구분해 사용할 수 있습니다.

function printWidth(
  width: Width
) {
  if (typeof width === "number") {
    console.log(
      `${width}px`
    );
  } else {
    console.log(
      "자동 너비를 사용합니다."
    );
  }
}

호출 결과:

printWidth(300);
printWidth("auto");

출력 결과:

300px
자동 너비를 사용합니다.

31. 설정 옵션에 리터럴 타입 사용하기

정렬 방향을 타입으로 만들어 보겠습니다.

type SortOrder =
  | "asc"
  | "desc";

정렬 함수를 작성합니다.

function sortNumbers(
  numbers: number[],
  order: SortOrder
): number[] {
  const copiedNumbers = [
    ...numbers,
  ];

  if (order === "asc") {
    return copiedNumbers.sort(
      (a, b) => a - b
    );
  }

  return copiedNumbers.sort(
    (a, b) => b - a
  );
}

오름차순 정렬:

const ascending =
  sortNumbers(
    [30, 10, 20],
    "asc"
  );

결과:

[10, 20, 30]

내림차순 정렬:

const descending =
  sortNumbers(
    [30, 10, 20],
    "desc"
  );

결과:

[30, 20, 10]

잘못된 옵션은 전달할 수 없습니다.

sortNumbers(
  [30, 10, 20],
  "up"
);

32. UI 상태를 리터럴 타입으로 관리하기

데이터를 불러오는 화면의 상태를 만들어 보겠습니다.

type LoadingStatus =
  | "idle"
  | "loading"
  | "success"
  | "error";

초기 상태:

let loadingStatus:
  LoadingStatus = "idle";

요청 시작:

loadingStatus = "loading";

요청 성공:

loadingStatus = "success";

요청 실패:

loadingStatus = "error";

상태에 따라 메시지를 출력할 수 있습니다.

function getStatusMessage(
  status: LoadingStatus
): string {
  if (status === "idle") {
    return "요청을 기다리고 있습니다.";
  }

  if (status === "loading") {
    return "데이터를 불러오는 중입니다.";
  }

  if (status === "success") {
    return "데이터를 불러왔습니다.";
  }

  return "오류가 발생했습니다.";
}

함수를 호출합니다.

console.log(
  getStatusMessage("loading")
);

출력 결과:

데이터를 불러오는 중입니다.

리터럴 타입을 사용하면 상태값의 철자와 종류를 한곳에서 통제할 수 있습니다.

33. 함수 오버로드 대신 유니언 타입을 사용할 수 있을까?

문자열 또는 숫자를 받는 간단한 함수는 유니언 타입으로 표현할 수 있습니다.

function formatValue(
  value: string | number
): string {
  if (typeof value === "number") {
    return value.toLocaleString();
  }

  return value.trim();
}

숫자 입력:

console.log(
  formatValue(120000)
);

출력 결과:

120,000

문자열 입력:

console.log(
  formatValue(
    "  TypeScript  "
  )
);

출력 결과:

TypeScript

입력 타입에 따라 반환 타입까지 달라지는 복잡한 함수라면 함수 오버로드가 필요할 수 있습니다.

함수 오버로드는 PART 5에서 다룰 예정입니다.

이번 단계에서는 여러 입력 타입을 받을 때 유니언 타입을 사용할 수 있다는 점만 기억하면 됩니다.

34. 유니언 타입에서 값 비교하기

리터럴 유니언 타입은 값 자체를 비교해 타입을 구분할 수 있습니다.

type PaymentMethod =
  | "card"
  | "bank"
  | "cash";
function pay(
  method: PaymentMethod
) {
  if (method === "card") {
    console.log(
      "카드 결제를 진행합니다."
    );
  } else if (method === "bank") {
    console.log(
      "계좌이체를 진행합니다."
    );
  } else {
    console.log(
      "현금 결제를 진행합니다."
    );
  }
}

호출:

pay("card");

출력 결과:

카드 결제를 진행합니다.

허용된 값이 정해져 있으므로 조건문에서도 가능한 경우를 쉽게 파악할 수 있습니다.

35. 기본값과 리터럴 타입

함수의 기본 매개변수에도 리터럴 타입을 사용할 수 있습니다.

type Theme =
  | "light"
  | "dark";

function applyTheme(
  theme: Theme = "light"
) {
  console.log(
    `${theme} 테마를 적용합니다.`
  );
}

인수를 생략하면 `"light"`가 사용됩니다.

applyTheme();

출력 결과:

light 테마를 적용합니다.

값을 전달할 수도 있습니다.

applyTheme("dark");

잘못된 값은 전달할 수 없습니다.

applyTheme("blue");

기본값도 `Theme` 타입에 포함된 값이어야 합니다.

function applyTheme(
  theme: Theme = "system"
) {}

`"system"`은 `Theme`에 없으므로 오류가 발생합니다.

36. 리터럴 타입 배열 만들기

특정 문자열만 담을 수 있는 배열을 만들 수 있습니다.

type Role =
  | "admin"
  | "manager"
  | "user";
const roles: Role[] = [
  "admin",
  "manager",
  "user",
];

허용된 값을 추가할 수 있습니다.

roles.push("admin");

허용되지 않은 값은 추가할 수 없습니다.

roles.push("guest");

숫자 리터럴 배열도 만들 수 있습니다.

type Rating =
  | 1
  | 2
  | 3
  | 4
  | 5;
const ratings: Rating[] = [
  5,
  4,
  3,
  5,
];

범위를 벗어난 값은 추가할 수 없습니다.

ratings.push(10);

37. `as const`와 리터럴 타입

다음 배열을 살펴보겠습니다.

const statuses = [
  "idle",
  "loading",
  "success",
  "error",
];

TypeScript는 일반적으로 `string[]`으로 추론합니다.

각 요소가 특정 문자열이지만 배열 요소가 변경될 수 있기 때문입니다.

statuses.push("anything");

`as const`를 사용하면 값과 순서를 고정할 수 있습니다.

const statuses = [
  "idle",
  "loading",
  "success",
  "error",
] as const;

추론 타입은 다음과 비슷합니다.

readonly [
  "idle",
  "loading",
  "success",
  "error"
]

이제 배열을 수정할 수 없습니다.

statuses.push("complete");

오류가 발생합니다.

`as const`는 11편의 타입 단언에서 더욱 자세히 다룰 예정입니다.

38. 배열에서 리터럴 타입 만들기

고정 배열에서 리터럴 유니언 타입을 만들 수 있습니다.

const themes = [
  "light",
  "dark",
  "system",
] as const;

배열 요소 타입을 추출하면 다음 타입을 만들 수 있습니다.

type Theme =
  typeof themes[number];

결과적으로 `Theme`은 다음과 같습니다.

type Theme =
  | "light"
  | "dark"
  | "system";

이 방법은 허용 목록과 타입을 한곳에서 관리할 때 유용합니다.

function setTheme(
  theme: Theme
) {
  console.log(theme);
}

다만 `typeof`, 인덱스 접근 타입 등은 이후 사용자 정의 타입 회차에서 더 자세히 다룰 수 있습니다.

지금은 다음 구조가 가능하다는 정도로 살펴보면 충분합니다.

고정 배열
↓
배열 요소들로 리터럴 유니언 타입 생성

39. 문자열 변수는 리터럴 타입 매개변수에 바로 전달되지 않을 수 있다

다음 함수를 살펴보겠습니다.

type Direction =
  | "left"
  | "right";

function move(
  direction: Direction
) {
  console.log(direction);
}

리터럴 값을 직접 전달하면 정상입니다.

move("left");

그런데 다음 코드는 오류가 발생할 수 있습니다.

let direction = "left";

move(direction);

`let direction = "left"`는 일반 `string`으로 추론되기 때문입니다.

direction = "forward";

나중에 어떤 문자열로든 바뀔 가능성이 있습니다.

해결 방법은 타입을 명시하는 것입니다.

let direction: Direction =
  "left";

move(direction);

또는 값이 변경되지 않는다면 `const`를 사용할 수 있습니다.

const direction = "left";

move(direction);

`const direction`은 `"left"` 리터럴 타입으로 추론됩니다.

40. 객체 속성의 리터럴 타입이 넓어지는 현상

다음 객체를 살펴보겠습니다.

const request = {
  method: "GET",
};

`const` 객체이지만 속성은 수정할 수 있습니다.

request.method = "POST";

따라서 `request.method`는 일반적으로 `string`으로 추론됩니다.

다음 함수가 특정 값만 받는다고 생각해 보겠습니다.

type HttpMethod =
  | "GET"
  | "POST";

function sendRequest(
  method: HttpMethod
) {
  console.log(method);
}

다음 코드에서 타입 오류가 발생할 수 있습니다.

sendRequest(request.method);

`request.method`가 일반 `string`이기 때문입니다.

객체 타입을 명시할 수 있습니다.

const request: {
  method: HttpMethod;
} = {
  method: "GET",
};

이제 정상적으로 전달할 수 있습니다.

sendRequest(
  request.method
);

또는 `as const`를 사용할 수도 있습니다.

const request = {
  method: "GET",
} as const;

이 경우 `method`는 `"GET"` 리터럴 타입으로 추론됩니다.

41. 유니언 타입을 매개변수마다 따로 사용하기

상품 검색 함수를 만들어 보겠습니다.

function searchProduct(
  keyword: string | number
) {
  if (
    typeof keyword === "number"
  ) {
    console.log(
      `상품 번호 ${keyword}로 검색합니다.`
    );

    return;
  }

  console.log(
    `"${keyword}" 키워드로 검색합니다.`
  );
}

숫자로 검색:

searchProduct(100);

출력:

상품 번호 100으로 검색합니다.

문자열로 검색:

searchProduct("키보드");

출력:

"키보드" 키워드로 검색합니다.

이처럼 동일한 기능에 여러 입력 방식을 제공할 때 유니언 타입을 활용할 수 있습니다.

42. 유니언 타입과 선택적 매개변수

선택적 매개변수는 자동으로 `undefined` 가능성을 포함합니다.

function printMessage(
  message?: string
) {
  console.log(message);
}

개념적으로 `message`는 다음과 비슷합니다.

string | undefined

따라서 문자열 메서드를 바로 호출할 수 없습니다.

function printMessage(
  message?: string
) {
  console.log(
    message.toUpperCase()
  );
}

값을 확인해야 합니다.

function printMessage(
  message?: string
) {
  if (
    message !== undefined
  ) {
    console.log(
      message.toUpperCase()
    );
  }
}

기본값을 사용할 수도 있습니다.

function printMessage(
  message?: string
) {
  const finalMessage =
    message ?? "메시지 없음";

  console.log(finalMessage);
}

유니언 타입은 새로운 문법으로만 등장하는 것이 아닙니다.

선택적 속성과 선택적 매개변수 뒤에도 조용히 숨어 있습니다.

43. 유니언 타입을 사용할 때 공통 속성을 찾자

다음 두 타입을 생각해 보겠습니다.

type TextOrArray =
  | string
  | string[];

문자열과 배열은 모두 `length` 속성을 가지고 있습니다.

function getLength(
  value: TextOrArray
): number {
  return value.length;
}

문자열 호출:

console.log(
  getLength("TypeScript")
);

배열 호출:

console.log(
  getLength([
    "JavaScript",
    "TypeScript",
  ])
);

두 타입 모두 사용할 수 있는 공통 속성이기 때문에 타입 확인 없이 접근할 수 있습니다.

하지만 `toUpperCase()`는 문자열에만 있습니다.

value.toUpperCase();

`push()`는 배열에만 있습니다.

value.push("Python");

공통 기능이 아니라면 타입을 구분해야 합니다.

44. 리터럴 타입에 한글 문자열도 사용할 수 있다

리터럴 타입은 영어만 사용할 수 있는 것이 아닙니다.

type OrderStatus =
  | "주문대기"
  | "결제완료"
  | "배송중"
  | "배송완료"
  | "주문취소";

다음처럼 사용할 수 있습니다.

let orderStatus:
  OrderStatus = "주문대기";

상태를 변경합니다.

orderStatus = "결제완료";
orderStatus = "배송중";

허용되지 않은 값은 저장할 수 없습니다.

orderStatus = "배달중";

다만 실제 프로젝트에서는 API 규격과 팀의 명명 규칙에 따라 영어 코드값을 사용하고, 화면에 표시할 한글은 별도로 관리하기도 합니다.

type OrderStatus =
  | "pending"
  | "paid"
  | "shipping"
  | "delivered"
  | "cancelled";

45. 실습 1: 사용자 ID 처리하기

문자열과 숫자 ID를 모두 처리하는 함수를 만들어 보겠습니다.

type UserId =
  | string
  | number;

ID를 정규화하는 함수를 작성합니다.

function normalizeUserId(
  id: UserId
): string {
  if (typeof id === "number") {
    return `USER-${id}`;
  }

  return id.toUpperCase();
}

함수를 호출합니다.

const firstId =
  normalizeUserId(100);

const secondId =
  normalizeUserId(
    "user-200"
  );

출력합니다.

console.log(firstId);
console.log(secondId);

출력 결과:

USER-100
USER-200

전체 코드입니다.

type UserId =
  | string
  | number;

function normalizeUserId(
  id: UserId
): string {
  if (typeof id === "number") {
    return `USER-${id}`;
  }

  return id.toUpperCase();
}

const firstId =
  normalizeUserId(100);

const secondId =
  normalizeUserId(
    "user-200"
  );

console.log(firstId);
console.log(secondId);

46. 실습 2: 알림 종류 제한하기

알림의 종류를 리터럴 타입으로 정의합니다.

type NotificationType =
  | "info"
  | "success"
  | "warning"
  | "error";

알림 메시지를 만드는 함수를 작성합니다.

function createNotification(
  type: NotificationType,
  message: string
): string {
  if (type === "info") {
    return `ℹ️ ${message}`;
  }

  if (type === "success") {
    return `✅ ${message}`;
  }

  if (type === "warning") {
    return `⚠️ ${message}`;
  }

  return `❌ ${message}`;
}

함수를 호출합니다.

console.log(
  createNotification(
    "success",
    "저장되었습니다."
  )
);

console.log(
  createNotification(
    "error",
    "저장에 실패했습니다."
  )
);

출력 결과:

✅ 저장되었습니다.
❌ 저장에 실패했습니다.

잘못된 타입은 전달할 수 없습니다.

createNotification(
  "complete",
  "완료되었습니다."
);

47. 실습 3: 배송 방법과 배송비 계산하기

배송 방법을 리터럴 타입으로 정의합니다.

type DeliveryMethod =
  | "standard"
  | "express"
  | "pickup";

배송비를 계산하는 함수를 작성합니다.

function calculateDeliveryFee(
  method: DeliveryMethod
): number {
  if (method === "standard") {
    return 3000;
  }

  if (method === "express") {
    return 7000;
  }

  return 0;
}

함수를 호출합니다.

const standardFee =
  calculateDeliveryFee(
    "standard"
  );

const expressFee =
  calculateDeliveryFee(
    "express"
  );

const pickupFee =
  calculateDeliveryFee(
    "pickup"
  );

출력합니다.

console.log(
  `일반 배송비: ${standardFee}원`
);

console.log(
  `빠른 배송비: ${expressFee}원`
);

console.log(
  `매장 수령 배송비: ${pickupFee}원`
);

출력 결과:

일반 배송비: 3000원
빠른 배송비: 7000원
매장 수령 배송비: 0원

다른 배송 방법은 사용할 수 없습니다.

calculateDeliveryFee(
  "rocket"
);

로켓 배송을 새로 지원하려면 타입과 함수 로직에 정식으로 등록해야 합니다. 🚀

48. 실습 4: 게임 난이도 설정하기

게임 난이도를 정의해 보겠습니다.

type Difficulty =
  | "easy"
  | "normal"
  | "hard";

난이도에 따라 적의 체력을 반환합니다.

function getEnemyHealth(
  difficulty: Difficulty
): number {
  if (difficulty === "easy") {
    return 50;
  }

  if (difficulty === "normal") {
    return 100;
  }

  return 200;
}

함수를 호출합니다.

const enemyHealth =
  getEnemyHealth("hard");

console.log(enemyHealth);

출력 결과:

200

다음 값은 전달할 수 없습니다.

getEnemyHealth(
  "impossible"
);

게임 디자이너가 아직 승인하지 않은 지옥 난이도는 타입 시스템이 먼저 봉인합니다. 🎮

49. 실습 5: 검색 조건 만들기

검색 키워드가 문자열 또는 숫자일 수 있다고 가정해 보겠습니다.

type SearchKeyword =
  | string
  | number;

검색 함수를 작성합니다.

function search(
  keyword: SearchKeyword
): string {
  if (
    typeof keyword === "number"
  ) {
    return `상품 번호 ${keyword} 검색`;
  }

  const trimmedKeyword =
    keyword.trim();

  if (
    trimmedKeyword.length === 0
  ) {
    return "검색어를 입력해 주세요.";
  }

  return `"${trimmedKeyword}" 검색`;
}

함수를 호출합니다.

console.log(
  search(100)
);

console.log(
  search(" 키보드 ")
);

console.log(
  search("   ")
);

출력 결과:

상품 번호 100 검색
"키보드" 검색
검색어를 입력해 주세요.

50. 자주 발생하는 실수

실수 1. 유니언 타입 값에 전용 메서드 바로 호출하기

function printValue(
  value: string | number
) {
  console.log(
    value.toUpperCase()
  );
}

숫자일 가능성이 있으므로 오류입니다.

타입을 확인합니다.

function printValue(
  value: string | number
) {
  if (
    typeof value === "string"
  ) {
    console.log(
      value.toUpperCase()
    );
  }
}

실수 2. `any`를 유니언 타입 대신 사용하기

let userId: any;

모든 값이 들어올 수 있습니다.

userId = true;
userId = {};

허용 범위가 정해져 있다면 유니언 타입을 사용합니다.

let userId:
  string | number;

실수 3. 리터럴 타입에 넓은 타입을 추가하기

type Status =
  | "loading"
  | "success"
  | string;

`string`이 포함되어 모든 문자열을 허용하게 됩니다.

let status: Status =
  "banana";

정확히 제한하려면 `string`을 제거합니다.

type Status =
  | "loading"
  | "success"
  | "error";

실수 4. 배열 타입 괄호를 빠뜨리기

원하는 타입:

(string | number)[]

잘못 이해하기 쉬운 타입:

string | number[]

첫 번째는 문자열과 숫자가 섞인 배열입니다.

두 번째는 문자열 하나 또는 숫자 배열입니다.

실수 5. 일반 문자열 변수를 리터럴 타입으로 전달하기

type Direction =
  | "left"
  | "right";

let direction = "left";

move(direction);

`direction`은 일반 `string`으로 추론됩니다.

타입을 명시합니다.

let direction: Direction =
  "left";

move(direction);

실수 6. 객체 속성이 자동으로 리터럴 타입이라고 생각하기

const request = {
  method: "GET",
};

`method`는 수정 가능하므로 일반 `string`으로 추론될 수 있습니다.

타입을 명시합니다.

type HttpMethod =
  | "GET"
  | "POST";

const request: {
  method: HttpMethod;
} = {
  method: "GET",
};

실수 7. 유니언 타입을 지나치게 넓게 만들기

type Input =
  | string
  | number
  | boolean
  | object
  | null
  | undefined;

사용하는 코드가 복잡해집니다.

실제 필요한 타입만 포함합니다.

type UserId =
  | string
  | number;

51. 유니언 타입과 리터럴 타입 선택 공식

타입을 설계할 때 다음 질문을 확인해 보세요.

질문 1. 값이 여러 타입 중 하나가 될 수 있는가?

유니언 타입을 사용합니다.

type UserId =
  string | number;

질문 2. 아무 문자열이 아니라 정해진 문자열만 허용해야 하는가?

문자열 리터럴 유니언을 사용합니다.

type Status =
  | "idle"
  | "loading"
  | "success"
  | "error";

질문 3. 제한된 숫자만 허용해야 하는가?

숫자 리터럴 유니언을 사용할 수 있습니다.

type Rating =
  | 1
  | 2
  | 3
  | 4
  | 5;

질문 4. 유니언 타입의 값에 타입별 기능을 사용해야 하는가?

먼저 실제 타입을 확인합니다.

if (
  typeof value === "string"
) {
  // 문자열 기능 사용
}

질문 5. 같은 유니언 타입을 여러 곳에서 사용하는가?

타입 별칭을 만듭니다.

type ProductId =
  string | number;

질문 6. 리터럴 목록에 `string`이나 `number`를 함께 넣었는가?

제한이 무력화되지 않는지 확인합니다.

type WrongStatus =
  | "loading"
  | "success"
  | string;

질문 7. 값의 종류가 지나치게 많은가?

데이터 구조를 다시 설계할 필요가 있는지 검토합니다.

유니언 타입이 너무 커지면 사용하는 모든 곳에서 타입 확인이 복잡해집니다.

52. 미니 퀴즈

문제 1

다음 타입의 의미는 무엇일까요?

let value:
  string | number;

정답

`value`에는 문자열 또는 숫자를 저장할 수 있습니다.

불리언이나 객체는 저장할 수 없습니다.

문제 2

다음 코드에서 오류가 발생하는 이유는 무엇일까요?

function printValue(
  value: string | number
) {
  console.log(
    value.toFixed(2)
  );
}

정답

`value`가 문자열일 수 있기 때문입니다.

`toFixed()`는 숫자에서만 사용할 수 있습니다.

if (
  typeof value === "number"
) {
  console.log(
    value.toFixed(2)
  );
}

문제 3

다음 타입은 어떤 값을 허용할까요?

type Direction =
  | "left"
  | "right";

정답

정확히 `"left"` 또는 `"right"`만 허용합니다.

문제 4

다음 코드에서 오류가 발생할까요?

type Rating =
  1 | 2 | 3 | 4 | 5;

let rating: Rating = 6;

정답

오류가 발생합니다.

`6`은 `Rating` 타입에 포함되어 있지 않습니다.

문제 5

다음 두 타입의 차이는 무엇일까요?

(string | number)[]
string | number[]

정답

첫 번째는 문자열 또는 숫자를 요소로 가지는 배열입니다.

두 번째는 문자열 하나 또는 숫자 배열입니다.

문제 6

다음 코드에서 `status`의 추론 타입은 무엇일까요?

let status = "loading";

정답

string

`let`은 나중에 다른 문자열로 변경될 수 있기 때문입니다.

문제 7

다음 코드에서 `status`의 추론 타입은 무엇일까요?

const status = "loading";

정답

"loading"

값을 다시 할당할 수 없으므로 문자열 리터럴 타입으로 추론됩니다.

문제 8

다음 타입의 문제는 무엇일까요?

type Status =
  | "loading"
  | "success"
  | string;

정답

`string`이 포함되어 있으므로 모든 문자열이 허용됩니다.

리터럴 타입으로 값을 제한하는 의미가 사라집니다.

문제 9

다음 함수가 숫자와 문자열을 안전하게 처리하도록 완성해 보세요.

function format(
  value: string | number
) {
  // 코드 작성
}

정답 예시

function format(
  value: string | number
): string {
  if (
    typeof value === "number"
  ) {
    return value.toFixed(2);
  }

  return value.toUpperCase();
}

문제 10

다음 버튼 크기 타입에 허용되는 값은 무엇일까요?

type ButtonSize =
  | "small"
  | "medium"
  | "large";

정답

다음 세 값만 허용됩니다.

small
medium
large

53. 핵심 정리

유니언 타입

여러 타입 중 하나를 허용합니다.

let userId:
  string | number;

`|`는 또는이라는 의미

string | number
문자열 또는 숫자

유니언 타입의 전용 기능은 타입 확인 후 사용

if (
  typeof value === "string"
) {
  console.log(
    value.toUpperCase()
  );
}

공통 기능은 바로 사용할 수 있음

function printValue(
  value: string | number
) {
  console.log(
    value.toString()
  );
}

리터럴 타입

특정 값 자체를 타입으로 사용합니다.

let direction: "left";

리터럴 타입과 유니언 타입 조합

type Direction =
  | "left"
  | "right"
  | "up"
  | "down";

상태값 제한

type Status =
  | "idle"
  | "loading"
  | "success"
  | "error";

숫자 범위 제한

type Rating =
  | 1
  | 2
  | 3
  | 4
  | 5;

`let`과 `const`의 추론 차이

let status = "loading";
// string
const status = "loading";
// "loading"

반복되는 타입에는 이름 붙이기

type UserId =
  string | number;

배열 괄호 주의하기

(string | number)[]

문자열과 숫자를 요소로 가지는 배열입니다.

54. 마무리

유니언 타입은 하나의 값에 여러 가능성을 담을 수 있게 해줍니다.

let userId:
  string | number;

하지만 여러 타입을 허용한다고 해서 아무 값이나 허용하는 것은 아닙니다.

userId = "USER-100";
userId = 100;
userId = true;

마지막 불리언 값은 허용되지 않습니다.

리터럴 타입은 허용 가능한 값을 더욱 구체적으로 제한합니다.

type Status =
  | "idle"
  | "loading"
  | "success"
  | "error";

일반 문자열 타입은 모든 문자열을 허용합니다.

let status: string;

리터럴 유니언 타입은 등록된 문자열만 허용합니다.

let status: Status;

두 개념을 한 문장으로 정리하면 다음과 같습니다.

유니언 타입은 선택 가능한 타입을 정한다.
리터럴 타입은 선택 가능한 값을 정한다.

유니언 타입은 여러 차선이 있는 도로입니다.

리터럴 타입은 각 차선의 목적지가 적힌 안내판입니다.

차선은 여러 개지만 지도에 없는 목적지로 순간이동할 수는 없습니다. 🚦

TypeScript는 가능성을 막는 언어가 아닙니다.

필요한 가능성은 충분히 허용하고, 의도하지 않은 가능성만 코드 밖으로 밀어내는 언어입니다.

55. 다음 편 예고

지금까지 우리는 여러 타입 중 하나를 허용하는 유니언 타입과, 특정 값만 허용하는 리터럴 타입을 살펴봤습니다.

type UserId =
  string | number;

type Status =
  | "idle"
  | "loading"
  | "success"
  | "error";

그런데 외부 API나 사용자의 입력처럼 어떤 타입이 들어올지 전혀 알 수 없다면 어떻게 해야 할까요?

let data: any;

`any`를 사용하면 어떤 값이든 저장할 수 있습니다.

data = "TypeScript";
data = 100;
data = true;
data = {};

매우 편리해 보이지만 TypeScript의 타입 검사가 거의 멈춰버립니다.

data.notExistingMethod();

그렇다면 타입을 모르는 값을 더 안전하게 받는 방법은 없을까요?

let safeData: unknown;

`unknown`도 모든 값을 받을 수 있지만, 실제 타입을 확인하기 전에는 함부로 사용할 수 없습니다.

반환값이 없는 함수에는 어떤 타입을 작성해야 할까요?

function printMessage(): void {
  console.log("메시지를 출력합니다.");
}

그리고 절대로 정상적으로 끝나지 않는 함수에는 왜 `never`라는 타입을 사용할까요?

function throwError(
  message: string
): never {
  throw new Error(message);
}

다음 편에서는 TypeScript에서 조금 특별한 역할을 맡고 있는 네 가지 타입을 살펴보겠습니다.

다음 이야기

[TypeScript 완전정복 #10] 타입 검사를 포기하거나 더 엄격하게 지키는 방법 | any, unknown, void, never 완벽 이해하기

  • `any`는 왜 편리하면서도 위험할까요?
  • `any`를 사용하면 TypeScript의 검사는 어떻게 될까요?
  • `unknown`은 `any`와 무엇이 다를까요?
  • `unknown` 값을 안전하게 사용하려면 어떻게 해야 할까요?
  • 반환값이 없는 함수에는 왜 `void`를 사용할까요?
  • `undefined`와 `void`는 같은 의미일까요?
  • `never`는 어떤 값도 가질 수 없다는 뜻일까요?
  • 오류를 던지거나 무한 반복하는 함수에는 왜 `never`가 사용될까요?
  • 실무에서는 네 가지 타입을 언제 선택해야 할까요?

다음 편에서는 타입 검사를 잠시 내려놓는 `any`, 확인 전까지 문을 잠그는 `unknown`, 빈손으로 돌아오는 `void`, 절대로 돌아오지 않는 `never`를 만나보겠습니다.

댓글

0

댓글을 불러오는 중입니다.