TypeScript(TS)의 핵심 타입 (Type) 시스템 완전 이해
12가지 기본 타입을 완벽하게 이해하고,
각 타입이 언제, 왜 사용되는지 학습합니다.
// 문자열 타입
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은 넓은 타입으로, 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 - 속성은 변경 가능
const로 선언하면 타입이 자동으로 좁아져 더 안전한 코드가 됩니다.
// 모든 숫자는 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;
// 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;
// 방법 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 - 못 찾을 수도 있음
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, 고정 길이 배열)로 만들어
각 요소가 리터럴 타입이 됩니다.
고정된 길이와 각 위치별 타입이 정해진 배열
// 기본 튜플 - 각 위치의 타입이 다름
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 - 기본값은 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 (숫자도 허용 - 주의!)
// 문자열 enum - 더 안전
enum Theme {
Light = "LIGHT",
Dark = "DARK",
Auto = "AUTO"
}
let current: Theme = Theme.Dark;
// current === "DARK"
// 숫자와 달리 역방향 매핑 없음
// Theme["DARK"] -> undefined
// const enum - 컴파일 (Compile) 시 값이 직접 삽입됨
const enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let c = Color.Red;
// 컴파일 결과: let c = "RED";
// enum 객체가 생성되지 않음!
// 장점: 번들 (Bundle, 최종 파일) 크기 감소
// 단점: 역방향 매핑 (값으로 이름 찾기) 불가
const enum을 고려하세요.
최근 트렌드는 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!
트리 쉐이킹 (Tree Shaking, 사용하지 않는 코드 자동 제거) 가능, JavaScript(JS)와의 호환성, 더 유연한 타입 추출
// 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: 모든 타입 허용, 체크 필수
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을 사용하세요.
타입 안전성을 유지하면서 유연성을 확보할 수 있습니다.
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 - 반환값이 없는 함수
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는 절대 발생하지 않는 값의 타입입니다.
함수가 정상적으로 종료되지 않거나, 도달 불가능한 코드에서 사용됩니다.
// 항상 에러를 던지는 함수
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;
}
}
새로운 타입이 추가되면 컴파일 에러로 알려줍니다
// 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;
}
}
개발자가 컴파일러 (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; // 위험하지만 가능
리터럴이란 코드에 직접 쓴 고정된 값을 말합니다. 리터럴 타입은 "정확히 이 값만 허용"하는 타입입니다.
// 리터럴 타입: 특정 값만 허용
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!
유니온이란 "합집합"이라는 뜻으로, 여러 타입 중 하나가 될 수 있다는 의미입니다. | 기호로 연결합니다.
// 여러 타입 중 하나가 될 수 있음
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 };
| 타입 | 설명 | 예시 |
|---|---|---|
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 |