Chapter 02

기본 타입

TypeScript(TS)의 핵심 타입 (Type) 시스템 완전 이해


← 목차로 돌아가기

TypeScript 타입 전체 지도

string number boolean array tuple (튜플) enum (열거형) any unknown void null undefined never

이번 챕터에서 배울 내용

12가지 기본 타입을 완벽하게 이해하고,
각 타입이 언제, 왜 사용되는지 학습합니다.

원시 타입: string

// 문자열 타입
let firstName: string = "홍길동";
let greeting: string = `안녕하세요, ${firstName}님!`;

// 템플릿 리터럴 (Literal, 고정된 값)도 string
let multiLine: string = `
  여러 줄의
  문자열도
  가능합니다.
`;

// string 메서드 자동완성 지원
firstName.toUpperCase();   // IDE가 모든 string 메서드를 제안
firstName.includes("홍");  // boolean 반환 타입도 자동 추론
주의: String(대문자)은 래퍼 (Wrapper) 객체 타입으로, JavaScript가 내부적으로 사용하는 특수 객체입니다. 코드 작성 시에는 항상 소문자 string을 사용하세요.

원시 타입 추론: let vs const

핵심 개념

let넓은 타입으로, const좁은 리터럴 타입으로 추론됩니다.

// let: 재할당 가능 → 넓은 타입으로 추론
let x = "hello";      // 타입: string (모든 문자열 가능)
x = "world";          // OK - 다른 문자열 할당 가능

let count = 42;        // 타입: number
count = 100;           // OK - 다른 숫자 할당 가능

// const: 재할당 불가 → 좁은 리터럴 타입으로 추론
const y = "hello";     // 타입: "hello" (정확히 이 값만)
const PI = 3.14;       // 타입: 3.14 (number가 아님!)
const flag = true;     // 타입: true (boolean이 아님!)

// 실전에서의 차이
let status = "active";        // string → "inactive" 등 다른 값 가능
const STATUS = "active";      // "active" → 정확히 이 값만 허용

// 객체의 경우 const여도 속성은 변경 가능
const user = { name: "Kim" }; // 타입: { name: string } (리터럴 아님!)
user.name = "Lee";            // OK - 속성은 변경 가능
Tip: 변하지 않는 상수는 const로 선언하면 타입이 자동으로 좁아져 더 안전한 코드가 됩니다.

원시 타입: number, boolean

number

// 모든 숫자는 number (정수/실수 구분 없음)
let integer: number = 42;
let float: number = 3.14;
let negative: number = -10;

// 다양한 표기법
let hex: number = 0xff;       // 16진수
let binary: number = 0b1010;  // 2진수
let octal: number = 0o744;    // 8진수
let big: number = 1_000_000;  // 구분자

// 특수 값
let inf: number = Infinity;
let nan: number = NaN;

boolean

// true 또는 false만 가능
let isDone: boolean = false;
let isActive: boolean = true;

// 비교 연산 결과도 boolean
let isAdult: boolean = 20 >= 18;
let hasPermission = true; // 추론

// 조건문에서 활용
function checkAge(age: number): boolean {
  return age >= 18;
}

// boolean은 논리 연산 가능
let canAccess = isActive && hasPermission;

배열 (Arrays)

// 방법 1: 타입[] 표기법 (권장)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

// 방법 2: Array<타입> 제네릭 표기법
let scores: Array<number> = [90, 85, 92];
let flags: Array<boolean> = [true, false, true];

// 배열 타입 안전성
numbers.push(6);       // OK
numbers.push("seven"); // Error! string은 안 됨

// 배열 메서드 (Method, 객체에 속한 함수)도 타입 추론 (Type Inference)
const doubled = numbers.map(n => n * 2);    // number[] - 각 요소를 2배로
const filtered = names.filter(n => n.length > 3);  // string[] - 3글자 초과만
const found = numbers.find(n => n > 3);     // number | undefined - 못 찾을 수도 있음
Tip: find()의 반환 타입이 number | undefined인 이유는 요소를 찾지 못할 수 있기 때문입니다.

읽기 전용 배열

// readonly 배열 - 수정 불가
const colors: readonly string[] = ["red", "green", "blue"];

colors.push("yellow");   // Error! readonly 배열에 push 불가
colors[0] = "black";     // Error! 요소 변경 불가

// ReadonlyArray 제네릭
const points: ReadonlyArray<number> = [1, 2, 3];

// as const - 모든 요소를 리터럴 타입으로 고정
const DIRECTIONS = ["north", "south", "east", "west"] as const;
// 타입: readonly ["north", "south", "east", "west"]

type Direction = typeof DIRECTIONS[number];
// 타입: "north" | "south" | "east" | "west"

핵심 포인트

as const는 배열을 읽기 전용 튜플 (Tuple, 고정 길이 배열)로 만들어 각 요소가 리터럴 타입이 됩니다.

튜플 (Tuples)

고정된 길이와 각 위치별 타입이 정해진 배열

// 기본 튜플 - 각 위치의 타입이 다름
let person: [string, number] = ["홍길동", 25];

// 각 요소 접근 시 올바른 타입으로 추론
let name = person[0];   // string
let age = person[1];    // number

// 타입 에러
person = [25, "홍길동"];     // Error! 순서가 다름
person = ["홍길동"];         // Error! 길이가 다름

// 구조 분해 할당과 함께 사용
const [userName, userAge] = person;
// userName: string, userAge: number

// 실용적인 예: React의 useState가 튜플을 반환
// const [count, setCount] = useState(0);
// 타입: [number, Dispatch<SetStateAction<number>>]

튜플 활용 패턴

// 선택적 요소가 있는 튜플
type Point2D = [number, number];
type Point3D = [number, number, number?];  // z는 선택적

const p1: Point2D = [10, 20];
const p2: Point3D = [10, 20, 30];
const p3: Point3D = [10, 20];       // OK - z 생략 가능

// 이름이 있는 튜플 (가독성 향상)
type UserEntry = [name: string, age: number, active: boolean];

const entry: UserEntry = ["Kim", 30, true];

// 나머지 요소가 있는 튜플
type StringNumberBooleans = [string, number, ...boolean[]];

const data: StringNumberBooleans = ["hello", 1, true, false, true];

// 함수에서 여러 값 반환
function getMinMax(nums: number[]): [min: number, max: number] {
  return [Math.min(...nums), Math.max(...nums)];
}
const [min, max] = getMinMax([3, 1, 4, 1, 5]);

열거형 (Enum) - 숫자형

열거형이란 관련된 상수들을 하나의 그룹으로 묶어 이름을 붙인 것입니다. 예: 방향(상/하/좌/우)

// 숫자형 enum - 기본값은 0부터 자동 증가
enum Direction {
  Up,      // 0
  Down,    // 1
  Left,    // 2
  Right    // 3
}

let dir: Direction = Direction.Up;
console.log(dir);              // 0
console.log(Direction[0]);     // "Up" (역방향 매핑)

// 시작 값을 지정할 수도 있음
enum StatusCode {
  OK = 200,
  NotFound = 404,
  InternalError = 500
}

function handleResponse(code: StatusCode) {
  if (code === StatusCode.OK) {
    console.log("성공!");
  }
}

handleResponse(StatusCode.OK);       // OK
handleResponse(200);                 // OK (숫자도 허용 - 주의!)

문자열 열거형 & const enum

문자열 enum

// 문자열 enum - 더 안전
enum Theme {
  Light = "LIGHT",
  Dark  = "DARK",
  Auto  = "AUTO"
}

let current: Theme = Theme.Dark;
// current === "DARK"

// 숫자와 달리 역방향 매핑 없음
// Theme["DARK"] -> undefined

const enum

// const enum - 컴파일 (Compile) 시 값이 직접 삽입됨
const enum Color {
  Red   = "RED",
  Green = "GREEN",
  Blue  = "BLUE"
}

let c = Color.Red;
// 컴파일 결과: let c = "RED";
// enum 객체가 생성되지 않음!

// 장점: 번들 (Bundle, 최종 파일) 크기 감소
// 단점: 역방향 매핑 (값으로 이름 찾기) 불가
Best Practice: 문자열 enum을 사용하면 디버깅이 쉽고 타입 안전성이 높아집니다. 성능이 중요하면 const enum을 고려하세요.

Enum 대안: as const 패턴

최근 트렌드는 enum 대신 as const 객체를 선호합니다.

// as const 객체 패턴 (enum 대안)
const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE",
  Pending: "PENDING",
} as const;

// 유니온 (Union) 타입으로 추출
type Status = typeof Status[keyof typeof Status];
// "ACTIVE" | "INACTIVE" | "PENDING"

function setStatus(status: Status) {
  console.log(status);
}

setStatus(Status.Active);    // OK
setStatus("ACTIVE");         // OK
setStatus("INVALID");        // Error!

왜 as const를 선호하는가?

트리 쉐이킹 (Tree Shaking, 사용하지 않는 코드 자동 제거) 가능, JavaScript(JS)와의 호환성, 더 유연한 타입 추출

any vs unknown

any - 위험한 탈출구

// any: 모든 타입 허용, 체크 없음
let anything: any = "hello";
anything = 42;
anything = true;

// 위험! 아무 연산이나 가능
anything.foo.bar;     // OK (런타임 (Runtime, 실행 중) 에러)
anything();           // OK (런타임 에러)
anything.toFixed(2);  // OK (런타임 에러)

// 다른 타입에 할당도 가능!
let num: number = anything; // OK!

unknown - 안전한 대안

// unknown: 모든 타입 허용, 체크 필수
let something: unknown = "hello";
something = 42;
something = true;

// 안전! 직접 사용 불가
something.foo;        // Error!
something();          // Error!
something.toFixed(2); // Error!

// 타입 확인 후에만 사용 가능
if (typeof something === "number") {
  something.toFixed(2); // OK!
}
규칙: any 대신 unknown을 사용하세요. 타입 안전성을 유지하면서 유연성을 확보할 수 있습니다.

unknown 실전 활용

JSON.parse 결과, API 응답 등 타입이 불확실한 데이터를 안전하게 다루는 방법

// JSON.parse 결과를 unknown으로 안전하게 처리
function parseJSON(text: string): unknown {
  return JSON.parse(text);
}

const data = parseJSON('{"name": "Kim", "age": 25}');

// 직접 접근 불가 - 에러!
// console.log(data.name);

// 타입 좁히기 (Type Narrowing)로 안전하게 접근
if (typeof data === "object" && data !== null && "name" in data) {
  console.log((data as { name: string }).name); // "Kim"
}

// API 응답 처리 패턴
async function fetchUser(id: number): Promise<unknown> {
  const response = await fetch(`/api/users/${id}`);
  return response.json(); // 결과 타입이 불확실
}

// 사용 시 타입 가드로 검증
async function displayUser(id: number) {
  const result = await fetchUser(id);
  if (typeof result === "object" && result !== null
      && "name" in result && "age" in result) {
    const user = result as { name: string; age: number };
    console.log(`${user.name}, ${user.age}세`);
  }
}

void, null, undefined

// void - 반환값이 없는 함수
function logMessage(msg: string): void {
  console.log(msg);
  // return 없음 또는 return;
}

// undefined - 값이 할당되지 않은 상태
let u: undefined = undefined;

function findUser(id: number): string | undefined {
  // 사용자를 찾지 못하면 undefined 반환
  const users = ["Alice", "Bob"];
  return users[id];
}

// null - 의도적으로 "값 없음"을 표현
let n: null = null;

function getElement(id: string): HTMLElement | null {
  return document.getElementById(id);  // 없으면 null
}

// strictNullChecks가 true일 때 (권장 설정)
let userName: string = null;          // Error! string에 null 불가
let nullableName: string | null = null; // OK! 유니온으로 null 허용

never 타입

핵심 개념

never절대 발생하지 않는 값의 타입입니다.
함수가 정상적으로 종료되지 않거나, 도달 불가능한 코드에서 사용됩니다.

// 항상 에러를 던지는 함수
function throwError(message: string): never {
  throw new Error(message);
}

// 무한 루프
function infiniteLoop(): never {
  while (true) { }
}

// 가장 유용한 패턴: 완전성 검사 (Exhaustive Check, 모든 경우를 빠짐없이 처리했는지 확인)
type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
  switch (shape) {
    case "circle":    return Math.PI * 10 * 10;
    case "square":    return 10 * 10;
    case "triangle":  return (10 * 10) / 2;
    default:
      // shape가 never 타입이면 모든 케이스를 처리한 것
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

never를 활용한 안전 장치

새로운 타입이 추가되면 컴파일 에러로 알려줍니다

// Shape에 "pentagon"을 추가하면?
type Shape = "circle" | "square" | "triangle" | "pentagon";

function getArea(shape: Shape): number {
  switch (shape) {
    case "circle":    return Math.PI * 10 * 10;
    case "square":    return 10 * 10;
    case "triangle":  return (10 * 10) / 2;
    // case "pentagon" 빠뜨림!
    default:
      const _exhaustive: never = shape;
      // Error! Type '"pentagon"' is not assignable to type 'never'
      // -> pentagon 케이스를 처리하지 않았다고 알려줌!
      return _exhaustive;
  }
}
Tip: 이 패턴은 유니온 타입에 새 멤버를 추가할 때 빠뜨린 처리를 컴파일 (Compile) 시점에 잡아주는 강력한 안전 장치입니다.

타입 단언 (Type Assertion)

개발자가 컴파일러 (Compiler)보다 타입을 더 잘 아는 경우 사용합니다.
"이 값은 이 타입이 확실해!"라고 TypeScript에게 직접 알려주는 방법입니다.

// as 구문 (권장)
const input = document.getElementById("myInput") as HTMLInputElement;
input.value = "Hello";  // HTMLInputElement로 단언했으므로 .value 접근 가능

// 앵글 브래킷 구문 (JSX에서는 사용 불가)
const canvas = <HTMLCanvasElement>document.getElementById("myCanvas");

// JSON 파싱 결과에 타입 지정
interface Config {
  host: string;
  port: number;
}
const config = JSON.parse(rawData) as Config;

// 이중 단언 (최후의 수단!)
const value = "hello" as unknown as number;  // 위험하지만 가능
Warning: 타입 단언은 타입 체크를 우회합니다. 런타임 에러의 원인이 될 수 있으니 신중하게 사용하세요.

리터럴 타입 (Literal Type)

리터럴이란 코드에 직접 쓴 고정된 값을 말합니다. 리터럴 타입은 "정확히 이 값만 허용"하는 타입입니다.

// 리터럴 타입: 특정 값만 허용
let direction: "up" | "down" | "left" | "right";

direction = "up";      // OK
direction = "diagonal"; // Error! 허용된 값이 아님

// 숫자 리터럴 타입
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3;   // OK
let roll2: DiceRoll = 7;  // Error!

// boolean 리터럴 타입
type True = true;

// const 변수는 자동으로 리터럴 타입
const PI = 3.14;        // 타입: 3.14 (number가 아님!)
let mutable = 3.14;     // 타입: number

// 함수 매개변수에서 활용
function setAlignment(align: "left" | "center" | "right") {
  // align은 정확히 3가지 값만 가능
}
setAlignment("center"); // OK
setAlignment("top");    // Error!

유니온 타입 (Union Type)

유니온이란 "합집합"이라는 뜻으로, 여러 타입 중 하나가 될 수 있다는 의미입니다. | 기호로 연결합니다.

// 여러 타입 중 하나가 될 수 있음
let id: string | number;
id = "abc-123";   // OK
id = 42;          // OK
id = true;        // Error!

// 유니온 타입과 타입 가드 (Type Guard, 타입을 확인하여 좁혀나가는 기법)
function printId(id: string | number) {
  if (typeof id === "string") {
    // 이 블록에서 id는 string (typeof로 타입을 확인했으므로)
    console.log(id.toUpperCase());
  } else {
    // 이 블록에서 id는 number
    console.log(id.toFixed(2));
  }
}

// 유니온 타입의 공통 멤버만 접근 가능
function getLength(value: string | number[]) {
  // string과 number[] 모두 .length가 있음
  return value.length;  // OK
}

// 실용적 예: API 응답 타입
type ApiResponse =
  | { status: "success"; data: string }
  | { status: "error"; message: string };

Chapter 2 정리

타입 설명 예시
string문자열"hello"
number숫자 (정수/실수)42, 3.14
boolean참/거짓true, false
array배열number[], Array<string>
tuple고정 길이 배열[string, number]
enum열거형enum Color { Red }
any모든 타입 (위험)사용 자제
unknown안전한 any타입 체크 후 사용
void반환값 없음(): void
never도달 불가에러, 완전성 검사
리터럴특정 값"up" | "down"
유니온여러 타입 중 하나string | number
Next

수고하셨습니다!


다음 챕터: 함수 →


← 목차로 돌아가기