@taemni

@taemni

안녕하세요, 차근차근 성장 중인 조태민입니다.

instagram
TypeScript

[TypeScript] 유틸리티 타입 (Utility Types)

November 29, 2025

타입스크립트는 개발자가 타입을 더 쉽고 유연하게 다룰 수 있도록 다양한 유틸리티 타입을 자체적으로 제공한다. 이는 우리가 앞서 배운 제네릭, 맵드 타입, 조건부 타입 등의 기능을 미리 조합해둔 '도구 상자'와 같다. 이번 포스팅에서는 실무에서 가장 자주 쓰이는 핵심 유틸리티 타입들을 직접 구현해 보며 그 원리를 파헤쳐 본다. ![image.png](https://akodhrjerwyxttclrzkq.supabase.co/storage/v1/object/public/images/images/1764409505318-jxf74iipsp.png) --- 1. 맵드 타입 기반 유틸리티 타입 이들은 주로 객체 타입의 속성을 변환하는 데 사용된다. 1-1. Partial<T> (부분 집합) 특정 객체 타입의 모든 프로퍼티를 선택적(Optional) 프로퍼티로 바꿔준다. [상황] 게시글(Post)을 작성하다가 임시 저장을 해야 한다. 아직 tags 같은 정보가 없을 수도 있다. TypeScript interface Post { title: string; tags: string[]; content: string; thumbnailURL?: string; } // ❌ 에러: tags가 없음 // const draft: Post = { title: "제목", content: "초안" }; // ✅ 해결: Partial 사용 const draft: Partial<Post> = { title: "제목", content: "초안" }; [구현 원리] 맵드 타입을 이용해 모든 키에 ?를 붙여준다 . TypeScript type Partial<T> = { [key in keyof T]?: T[key]; }; 1-2. Required<T> (필수 집합) Partial과 반대로, 모든 프로퍼티를 필수(Required) 프로퍼티로 바꿔준다. [상황] 원래는 thumbnailURL이 선택적이었지만, 마케팅용 게시글에는 썸네일이 반드시 있어야 한다. TypeScript // ✅ 해결: Required 사용 -> thumbnailURL이 없으면 에러 발생 const withThumbnail: Required<Post> = { title: "홍보글", tags: ["ts"], content: "내용", thumbnailURL: "https://..." // 필수! }; [구현 원리] -?를 사용하여 선택적 속성(?)을 제거한다 . TypeScript type Required<T> = { [key in keyof T]-?: T[key]; }; 1-3. Readonly<T> (읽기 전용) 모든 프로퍼티를 읽기 전용(Readonly) 으로 만들어 수정할 수 없게 한다. [상황] 절대 수정되면 안 되는 중요한 게시글 데이터를 보호하고 싶다. TypeScript const protectedPost: Readonly<Post> = { title: "공지사항", tags: [], content: "수정 불가", }; // protectedPost.content = "해킹"; // ❌ 에러 발생 [구현 원리] readonly 키워드를 붙여준다 . TypeScript type Readonly<T> = { readonly [key in keyof T]: T[key]; }; 1-4. Pick<T, K> (골라내기) 객체 타입에서 특정 프로퍼티만 골라내어 새로운 타입을 만든다. [상황] 태그 기능이 없던 시절의 옛날 게시글 데이터를 다뤄야 한다. tags가 없는 타입이 필요하다. TypeScript // title과 content만 뽑아냄 const legacyPost: Pick<Post, "title" | "content"> = { title: "옛날 글", content: "내용", }; [구현 원리]K는 T의 키들(keyof T) 중 일부여야 한다(extends). 그 키들(K)만 순회하며 타입을 만든다 . TypeScript type Pick<T, K extends keyof T> = { [key in K]: T[key]; }; 1-5. Omit<T, K> (제외하기) 객체 타입에서 특정 프로퍼티만 제거한 새로운 타입을 만든다. Pick의 반대다. [상황] 제목이 없는 게시글도 존재할 수 있다. title만 뺀 타입이 필요하다. TypeScript // title만 제외함 const noTitlePost: Omit<Post, "title"> = { content: "내용", tags: [], thumbnailURL: "", }; [구현 원리]Pick과 Exclude를 조합해서 만든다. 전체 키(keyof T)에서 K를 뺀 나머지 키들을 Pick하는 방식이다 . TypeScript type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; 1-6. Record<K, V> (동일한 타입 반복) 객체의 키(K)와 값(V)의 타입을 정의하여, 동일한 패턴을 가진 객체를 쉽게 만든다. [상황] 썸네일 이미지를 크기별(large, medium, small, watch)로 관리해야 한다. TypeScript type Thumbnail = Record<"large" | "medium" | "small" | "watch", { url: string }>; / 결과 타입: { large: { url: string }, medium: { url: string }, ... } / [구현 원리] 키(K)가 keyof any(string | number | symbol)를 상속받도록 하고, 모든 키에 대해 값(V)을 할당한다 . TypeScript type Record<K extends keyof any, V> = { [key in K]: V; }; --- 2. 조건부 타입 기반 유틸리티 타입 조건부 타입(extends ? :)을 이용해 타입을 걸러내거나 추출한다. 2-1. Exclude<T, U> (제거) 유니온 타입 T에서 U와 겹치는 타입을 제거한다. TypeScript type A = Exclude<string | boolean, string>; // boolean [구현 원리] TypeScript type Exclude<T, U> = T extends U ? never : T; 2-2. Extract<T, U> (추출) 유니온 타입 T에서 U와 겹치는(할당 가능한) 타입만 추출한다. TypeScript type B = Extract<string | boolean, boolean>; // boolean [구현 원리] TypeScript type Extract<T, U> = T extends U ? T : never; 2-3. ReturnType<T> (반환 타입 추출) 함수 타입 T의 반환값 타입을 추출한다. TypeScript function funcA() { return "hello"; } type ReturnA = ReturnType<typeof funcA>; // string [구현 원리] infer 키워드를 사용해 반환 타입을 추론(R)한다 . TypeScript type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never; --- 요약 타입스크립트가 제공하는 유틸리티 타입은 복잡한 타입을 매우 간단하게 조작할 수 있게 해준다. 직접 구현 원리를 이해하면 필요할 때 커스텀 유틸리티 타입을 만들어 쓸 수도 있으니 잘 익혀두자. - 변형: Partial, Required, Readonly - 선택/제외: Pick, Omit - 구조 생성: Record - 집합 연산: Exclude, Extract - 추론: ReturnType

TypeScriptFrontEndStudyUtilityTypesAdvancedTypes
TypeScript

[TypeScript] 조건부 타입 (Conditional Types)

November 29, 2025

타입스크립트의 조건부 타입은 마치 프로그래밍의 삼항 연산자처럼, 조건에 따라 타입을 결정할 수 있게 해주는 강력한 문법이다. 제네릭과 결합했을 때 그 진가를 발휘하며, 유틸리티 타입을 만드는 핵심 원리이기도 하다. 이번 포스팅에서는 조건부 타입의 기초부터 infer 키워드까지 핵심 내용을 정리해 본다. 1. 조건부 타입 기초 기본 문법은 삼항 연산자와 매우 유사하다. extends 키워드를 사용하여 조건식을 만든다 . TypeScript // T가 U의 서브 타입이면 X, 아니면 Y T extends U ? X : Y 간단한 예제를 보자. TypeScript type A = number extends string ? number : string; number는 string의 서브 타입이 아니므로 조건식은 거짓이 되고, 결과적으로 type A는 string이 된다 . 제네릭과 함께 사용하기 조건부 타입은 제네릭과 함께 쓸 때 훨씬 유용하다. TypeScript type StringNumberSwitch<T> = T extends number ? string : number; let varA: StringNumberSwitch<number>; // string (참) let varB: StringNumberSwitch<string>; // number (거짓) 실전 예제: removeSpaces 함수 공백을 제거하는 함수를 만들 때, 입력값이 string이면 반환값도 string, undefined면 반환값도 undefined가 되도록 타입을 정교하게 만들 수 있다 . 함수 오버로딩 시그니처와 조건부 타입을 결합하면 구현부의 타입을 안전하게 지키면서도 호출자에게 정확한 타입을 제공할 수 있다 . TypeScript // 함수 오버로딩 시그니처 + 조건부 타입 활용 function removeSpaces<T>(text: T): T extends string ? string : undefined; function removeSpaces(text: any) { if (typeof text === "string") { return text.replaceAll(" ", ""); } else { return undefined; } } let result = removeSpaces("hi im winterlood"); // string 타입 let result2 = removeSpaces(undefined); // undefined 타입 --- 2. 분산적인 조건부 타입 (Distributive Conditional Types) 만약 조건부 타입의 제네릭(T)에 유니온 타입을 할당하면 어떻게 될까? TypeScript type StringNumberSwitch<T> = T extends number ? string : number; let c: StringNumberSwitch<number | string>; // 결과: string | number 결과가 number 하나로 퉁쳐지는 것이 아니라 string | number가 되었다. 그 이유는 분산적인 조건부 타입이 동작했기 때문이다 . 동작 원리 제네릭에 유니온 타입이 들어오면, 타입스크립트는 각 타입을 분리해서 개별적으로 연산을 수행한 뒤 다시 합친다 . 1. 분리: StringNumberSwitch<number> | StringNumberSwitch<string> 2. 계산: (number extends number ? string : ...) | (string extends number ? ... : number) 3. 결과: string | number 활용: Exclude 타입 구현하기 이 특징을 이용하면 특정 타입을 제거하는 Exclude 유틸리티 타입을 직접 구현할 수 있다 . TypeScript type Exclude<T, U> = T extends U ? never : T; type A = Exclude<number | string | boolean, string>; 동작 과정 : 1. number extends string? ❌ -> number 2. string extends string? ✅ -> never 3. boolean extends string? ❌ -> boolean 결과: number | never | boolean 최종: number | boolean (유니온에서 never는 공집합이므로 사라진다 ) --- 3. infer 키워드 (타입 추론) infer는 조건부 타입 내에서 특정 타입을 추론해서 가져올 때 사용하는 키워드다. "이 위치에 오는 타입을 R이라고 부르고, 나중에 써먹자!"라는 의미다. 함수 반환 타입 추출하기 (ReturnType) TypeScript type ReturnType<T> = T extends () => infer R ? R : never; type FuncA = () => string; type FuncB = () => number; type A = ReturnType<FuncA>; // string type B = ReturnType<FuncB>; // number 해석: T가 함수 타입이라면, 그 함수의 반환 타입을 R이라고 추론하고, 결과로 R을 반환해라. (아니면 never) . Promise 내부 타입 추출하기 TypeScript type PromiseUnpack<T> = T extends Promise<infer R> ? R : never; type PromiseA = PromiseUnpack<Promise<number>>; // number type PromiseB = PromiseUnpack<Promise<string>>; // string 해석: T가 Promise 타입이라면, 그 안에 들어있는 타입을 R이라고 추론하고 꺼내줘라. --- 요약 1. 조건부 타입: T extends U ? X : Y 형태로 조건에 따라 타입을 결정한다. 2. 분산 조건부 타입: 제네릭에 유니온 타입을 넘기면 각 타입에 대해 개별적으로 연산이 수행된다. (Exclude 등의 원리) 3. infer: 조건부 타입 안에서 특정 부분의 타입을 추론하여 변수처럼 가져다 쓸 수 있다. (ReturnType 등의 원리)

TypeScriptStudyAdvancedTypesInfer
TypeScript

[TypeScript] 타입 조작하기

November 29, 2025

타입스크립트에는 기본 타입을 상황에 따라 유동적으로 변환할 수 있는 강력한 기능들이 있다. 제네릭(Generic)도 그중 하나지만, 이번에는 제네릭과 조건부 타입을 제외한 4가지 핵심 타입 조작 기능을 정리해 본다. 1. 인덱스드 엑세스 타입 (Indexed Access Types) 2. keyof 연산자 3. 맵드 타입 (Mapped Types) 4. 템플릿 리터럴 타입 (Template Literal Types) --- 1. 인덱스드 엑세스 타입 (Indexed Access Types) 인덱스를 이용해 다른 타입 내의 특정 프로퍼티 타입을 추출하는 기능이다. 객체, 배열, 튜플에 모두 사용할 수 있다. 1-1. 객체 프로퍼티 타입 추출 게시글(Post) 타입에서 작성자(author)의 타입만 따로 떼어내고 싶을 때 유용하다. TypeScript interface Post { title: string; content: string; author: { id: number; name: string; age: number; }; } // Post["author"]를 통해 author 프로퍼티의 타입을 추출 function printAuthorInfo(author: Post["author"]) { console.log(${author.id} - ${author.name}); } 이렇게 하면 Post 타입이 수정되더라도 printAuthorInfo 함수의 매개변수 타입을 일일이 수정할 필요가 없다. 주의할 점: - 인덱스에는 값이 아닌 타입만 들어갈 수 있다. 변수에 담긴 문자열(const key = "author")을 인덱스로 쓸 수 없다. - 존재하지 않는 프로퍼티 키를 넣으면 오류가 발생한다. 1-2. 배열 요소 타입 추출 배열 타입 뒤에 [number]를 붙이면 배열 요소의 타입을 추출할 수 있다. TypeScript type PostList = { title: string; content: string; author: { id: number; name: string; age: number; }; }[]; // 배열의 요소 타입 추출 const post: PostList[number] = { title: "게시글 제목", content: "게시글 본문", author: { id: 1, name: "조태민", age: 25, }, }; PostList[0]처럼 리터럴 숫자를 넣어도 동일하게 동작한다. --- 2. keyof 연산자 객체 타입으로부터 모든 프로퍼티의 키(Key)를 String Literal Union 타입으로 추출하는 연산자다. TypeScript interface Person { name: string; age: number; location: string; } // keyof Person 결과: "name" | "age" | "location" function getPropertyKey(person: Person, key: keyof Person) { return person[key]; } 프로퍼티가 추가되거나 이름이 바뀌어도 keyof가 자동으로 반영해주므로 유지보수가 매우 편해진다. 꿀팁: typeof와 함께 사용하기 변수의 타입을 가져오는 typeof 연산자와 함께 사용하면 객체 변수로부터 타입을 뽑아내고, 그 키들을 추출할 수 있다. TypeScript const person = { name: "조태민", age: 25, }; // typeof person -> Person 객체 타입 추론 // keyof typeof person -> "name" | "age" function getPropertyKey(person: typeof person, key: keyof typeof person) { return person[key]; } --- 3. 맵드 타입 (Mapped Types) 기존 객체 타입을 기반으로 새로운 객체 타입을 만드는 기능이다. 자바스크립트의 map 함수와 비슷하게 동작한다. 예를 들어, 모든 프로퍼티를 선택적(?)으로 바꾸는 PartialUser 타입을 만들어보자. TypeScript interface User { id: number; name: string; age: number; } // 맵드 타입 정의 type PartialUser = { [key in keyof User]?: User[key]; }; - [key in keyof User]: User의 키들(id | name | age)을 순회한다. - User[key]: 해당 키의 타입을 가져온다. - ?: 모든 프로퍼티를 선택적 속성으로 만든다. 결과적으로 PartialUser는 다음과 같은 타입이 된다 5. TypeScript { id?: number; name?: string; age?: number; } 이를 활용하면 모든 속성을 읽기 전용(readonly)으로 만드는 등 다양하게 변형할 수 있다. TypeScript type ReadonlyUser = { readonly [key in keyof User]: User[key]; }; --- 4. 템플릿 리터럴 타입 (Template Literal Types) 문자열 리터럴 타입을 조합하여 새로운 문자열 패턴 타입을 만드는 기능이다. TypeScript type Color = "red" | "black" | "green"; type Animal = "dog" | "cat" | "chicken"; // 모든 경우의 수 조합 (ex: "red-dog", "black-cat" ...) type ColoredAnimal = ${Color}-${Animal}; 일일이 조합을 적지 않아도 Color와 Animal의 모든 가능한 조합을 자동으로 생성해 준다 6. --- 요약 | 기능 | 설명 | 예시 문법 | | --- | --- | --- | | 인덱스드 엑세스 | 특정 프로퍼티/요소의 타입 추출 | Type["key"], ArrayType[number] | | keyof | 객체 타입의 키를 유니온으로 추출 | keyof Type | | 맵드 타입 | 기존 타입을 순회하며 변형 | [Key in keyof Type]: Type[Key] | | 템플릿 리터럴 | 문자열 패턴 조합 | ${TypeA}-${TypeB} | 이 기능들을 적절히 활용하면 중복 코드를 줄이고, 유지보수하기 쉬운 유연한 타입을 설계할 수 있다.

TypeScriptStudyTypeManipulationAdvancedTypes
TypeScript

[TypeScript] 제네릭(Generic) 응용

November 29, 2025

지난 포스팅에서 제네릭의 기본 개념(T를 변수처럼 사용)을 익혔다. 이번에는 다양한 상황에서 제네릭을 어떻게 응용할 수 있는지 알아본다. 1. 제네릭 함수 응용 1-1. 다중 타입 변수 (T, U) 필요하다면 타입 변수를 2개 이상 선언해서 사용할 수 있다. TypeScript function swap<T, U>(a: T, b: U) { return [b, a]; } const [a, b] = swap("1", 2); // a는 number, b는 string 타입으로 자동 추론됨 1-2. 배열 타입 다루기 (T[]) 배열의 요소 타입을 제네릭으로 받으면, 다양한 타입의 배열을 처리하는 함수를 만들 수 있다. TypeScript function returnFirstValue<T>(data: T[]) { return data[0]; } let num = returnFirstValue([0, 1, 2]); // T는 number, 반환값 number let str = returnFirstValue([1, "hello", "mynameis"]); // T는 number | string 💡 튜플로 첫 번째 요소 타입 확정하기 만약 배열의 첫 번째 요소 타입을 정확히 추론하고 싶다면 튜플과 나머지 파라미터를 활용한다. TypeScript function returnFirstValue<T>(data: [T, ...unknown[]]) { return data[0]; } let str = returnFirstValue([1, "hello", "mynameis"]); // 반환값이 정확히 number로 추론됨 (유니온 타입 아님) 1-3. 타입 변수 제한하기 (extends) 타입 변수에 아무 타입이나 들어오는 것을 막고, 특정 조건을 만족하는 타입만 받도록 제한할 수 있다. TypeScript // length 프로퍼티가 있는 타입만 허용 function getLength<T extends { length: number }>(data: T) { return data.length; } getLength("123"); // ✅ (string은 length 있음) getLength([1, 2, 3]); // ✅ (array는 length 있음) getLength({ length: 1 }); // ✅ (객체에 length 있음) // getLength(undefined); // ❌ (length 없음) --- 2. map & forEach 메서드 직접 구현하기 자바스크립트의 내장 메서드인 map과 forEach도 제네릭을 이용해 타입을 정의할 수 있다. map 메서드 타입 정의 map은 원본 배열의 타입(T)과 변환된 배열의 타입(U)이 다를 수 있으므로, 두 개의 타입 변수가 필요하다. TypeScript function map<T, U>(arr: T[], callback: (item: T) => U): U[] { let result = []; for (let i = 0; i < arr.length; i++) { result.push(callback(arr[i])); } return result; } const arr = [1, 2, 3]; const result = map(arr, (it) => it.toString()); // result는 string[] 타입 ["1", "2", "3"] forEach 메서드 타입 정의 forEach는 반환값이 없으므로(void), 비교적 간단하게 정의할 수 있다. TypeScript function forEach<T>(arr: T[], callback: (item: T) => void) { for (let i = 0; i < arr.length; i++) { callback(arr[i]); } } --- 3. 제네릭 인터페이스 & 타입 별칭 3-1. 제네릭 인터페이스 인터페이스 이름 뒤에 <T> 등을 붙여 정의한다. TypeScript interface KeyPair<K, V> { key: K; value: V; } // ⚠️ 주의: 변수 정의 시 반드시 타입을 명시해야 함 let keyPair: KeyPair<string, number> = { key: "key", value: 0, }; 3-2. 인덱스 시그니처와 함께 사용 TypeScript interface Map<V> { [key: string]: V; } let booleanMap: Map<boolean> = { key: true, }; 3-3. 제네릭 타입 별칭 인터페이스와 동일하게 사용 가능하다. TypeScript type Map2<V> = { [key: string]: V; }; --- 4. 실전 예제: 제네릭 인터페이스 활용 유저의 프로필(profile)이 상황에 따라 Student일 수도 있고 Developer일 수도 있다면? 제네릭을 이용해 중복 코드를 없애고 타입 안정성을 높일 수 있다. TypeScript interface Student { type: "student"; school: string; } interface Developer { type: "developer"; skill: string; } // 제네릭 인터페이스 정의 interface User<T> { name: string; profile: T; } // 학생만 이용 가능한 함수: 매개변수 타입을 User<Student>로 제한 function goToSchool(user: User<Student>) { const school = user.profile.school; // 타입 좁히기 없이 바로 접근 가능! console.log(${school}로 등교 완료); } 함수 내부에서 if문으로 타입을 좁힐 필요가 없어져 코드가 훨씬 깔끔해진다. --- 5. 제네릭 클래스 클래스도 제네릭을 사용하면 하나의 클래스로 다양한 타입의 데이터를 처리할 수 있다. TypeScript class List<T> { constructor(private list: T[]) {} push(data: T) { this.list.push(data); } print() { console.log(this.list); } } // 생성자 인수로 타입 추론 가능 const numberList = new List([1, 2, 3]); // T는 number const stringList = new List(["a", "b"]); // T는 string 제네릭이 없다면 NumberList, StringList 클래스를 따로 만들어야 했을 것이다. --- 6. 프로미스(Promise)와 제네릭 비동기 처리에 사용되는 Promise는 제네릭 클래스로 구현되어 있다. 성공했을 때(resolve) 반환되는 값의 타입을 제네릭으로 지정해 줄 수 있다. TypeScript // 1. 생성 시점에 타입 지정 const promise = new Promise<number>((resolve, reject) => { setTimeout(() => { resolve(20); }, 3000); }); promise.then((response) => { // response는 number 타입 (20) console.log(response); }); 함수의 반환값으로 사용할 때는 다음과 같이 명시하는 것이 직관적이다. TypeScript interface Post { id: number; title: string; content: string; } // 반환값 타입에 Promise<Post> 명시 function fetchPost(): Promise<Post> { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ id: 1, title: "게시글 제목", content: "게시글 본문", }); }, 3000); }); } > 참고: reject로 전달되는 에러 값의 타입은 정의할 수 없으며, 기본적으로 unknown (또는 any)으로 처리된다. > --- 요약 1. 함수 응용: 다중 타입 변수, 배열 처리, extends를 이용한 타입 제한 등 다양하게 활용 가능. 2. 인터페이스/타입 별칭: 객체의 프로퍼티 타입을 유연하게 변경하며 재사용성 극대화. 3. 클래스: 하나의 클래스로 여러 타입의 데이터를 처리하는 범용 클래스 생성. 4. Promise: 비동기 작업의 결과값 타입을 제네릭으로 정의하여 안전하게 처리.

TypeScriptStudyGenericsPromise
TypeScript

[TypeScript] 제네릭(Generic)

November 29, 2025

타입스크립트를 공부하다 보면 함수나 인터페이스 뒤에 붙은 <T> 같은 기호를 본 적이 있을 것이다. 이것이 바로 제네릭(Generic) 이다. 제네릭은 단일 타입이 아닌 다양한 타입에서 동작하는 컴포넌트를 작성할 수 있게 해주는 타입스크립트의 놀라운 기능이다. 쉽게 말해 타입을 변수처럼 유연하게 다루는 도구라고 볼 수 있다. 1. 제네릭이 필요한 이유 제네릭을 왜 써야 하는지 이해하기 위해, 인수로 받은 값을 그대로 반환하는 간단한 함수 func를 만들어보자. 시도 1: any 타입 사용하기 다양한 타입을 받아야 하니, 일단 매개변수 타입을 any로 설정해 보았다. TypeScript function func(value: any) { return value; } let num = func(10); let str = func("string"); 이 코드는 잘 동작하는 것처럼 보이지만 치명적인 단점이 있다. 반환값의 타입마저 any가 되어버린다는 점이다. TypeScript num.toUpperCase(); // ❌ 런타임 오류 발생! num에는 분명 숫자 10이 들어있지만, 타입스크립트는 이를 any로 인식하기 때문에 문자열 메서드인 .toUpperCase()를 써도 에러를 잡아내지 못한다. 결국 실행 시점에 프로그램이 뻗어버리는 위험한 상태가 된다. 시도 2: unknown 타입 사용하기 그렇다면 any보다 안전한 unknown을 쓰면 어떨까? TypeScript function func(value: unknown) { return value; } let num = func(10); // num의 타입은 unknown // num.toFixed(); // ❌ 오류 발생 (unknown에는 메서드 사용 불가) unknown은 안전하지만 너무 엄격하다. 값을 사용하려면 매번 typeof 등을 이용해 타입 좁히기를 해야 한다. TypeScript if (typeof num === "number") { num.toFixed(); // 귀찮은 과정이 필요함 } 우리가 원하는 건 단순하다. "숫자를 넣으면 숫자가 반환되고, 문자를 넣으면 문자가 반환되는" 그런 유연한 함수다. 이럴 때 제네릭이 필요하다. --- 2. 제네릭(Generic) 함수 제네릭(Generic) 이라는 단어는 '일반적인', '포괄적인'이라는 뜻을 가진다. 즉, 모든 타입의 값을 두루두루 포괄하여 적용할 수 있는 범용적인 함수를 만든다는 의미다. 제네릭 기본 문법 <T> 제네릭 함수는 함수 이름 뒤에 꺾쇠 < >를 열고 타입 변수를 선언하여 사용한다. 보통 T (Type의 약자)를 많이 쓴다. TypeScript // <T>: 타입 변수 선언 // (value: T): 매개변수 타입 // : T : 반환값 타입 function func<T>(value: T): T { return value; } 이제 이 함수를 호출할 때 전달하는 인수의 타입에 따라 T가 결정된다. TypeScript let num = func(10); // 1. 인수 10(number)이 전달됨 // 2. T가 number로 추론됨 // 3. 매개변수와 반환값 타입이 모두 number로 결정됨 // 결과: num은 number 타입 ✅ TypeScript let str = func("string"); // 1. 인수 "string"이 전달됨 // 2. T가 string으로 추론됨 // 결과: str은 string 타입 ✅ 이제 any처럼 타입 안정성을 잃지도 않고, unknown처럼 귀찮게 타입을 좁힐 필요도 없다. --- 3. 명시적으로 타입 지정하기 대부분의 경우 타입스크립트가 타입을 알아서 잘 추론해 주지만, 가끔은 우리가 직접 타입을 지정해야 할 때가 있다. 예를 들어 배열을 넣었을 때, 튜플 타입으로 추론되길 원한다면 다음과 같이 명시할 수 있다. TypeScript // 자동으로 추론시키면? let arr = func([1, 2, 3]); // T는 number[] 로 추론됨 // 명시적으로 지정하면? let tuple = func<[number, number, number]>([1, 2, 3]); // T는 [number, number, number] 튜플 타입으로 결정됨 함수 호출 시 <타입>을 직접 적어주면, 타입스크립트는 추론하지 않고 우리가 적어준 타입을 T에 할당한다. --- 요약 1. 문제점: any는 타입 검사를 포기하고, unknown은 사용하기 너무 번거롭다. 2. 해결책: 제네릭을 사용하면 입력값의 타입에 따라 반환값의 타입이 유연하게 결정된다. 3. 문법: 함수 이름 뒤에 <T>를 붙여 타입 변수를 선언하고 사용한다. 4. 사용: 일반적으로는 타입이 자동 추론되지만, 필요하다면 <Type>을 명시하여 호출할 수 있다.

TypeScriptStudyGenerics기초문법
TypeScript

[TypeScript] 인터페이스 구현하기 (implements)

November 27, 2025

타입스크립트의 인터페이스(Interface) 는 객체의 타입을 정의하는 용도뿐만 아니라, 클래스의 설계도(Blueprint) 역할도 수행할 수 있다. 쉽게 말해, "이 클래스는 적어도 이 프로퍼티와 이 메서드는 무조건 가지고 있어야 해!"라고 강제하는 것이다. 이때 사용하는 키워드가 바로 implements다. 1. 인터페이스로 설계도 만들기 먼저 게임 캐릭터를 만든다고 가정하고, 캐릭터라면 가져야 할 필수 요소를 인터페이스로 정의해보자. TypeScript interface CharacterInterface { name: string; // 이름 moveSpeed: number; // 이동 속도 move(): void; // 이동 메서드 } 이제 이 인터페이스는 하나의 약속(Contract) 이 된다. "누구든 CharacterInterface를 구현하려면 name, moveSpeed, move()를 반드시 가지고 있어야 한다"는 규칙이 생긴 것이다. --- 2. 클래스에서 구현하기 (implements) 클래스 이름 뒤에 implements 키워드와 인터페이스 이름을 적어주면 된다. TypeScript class Character implements CharacterInterface { // 생성자 매개변수에 접근 제어자를 붙여 필드 선언과 초기화를 한 번에 해결 constructor( public name: string, public moveSpeed: number, private extra: string // 인터페이스에 없는 건 마음대로 추가 가능 ) {} // 인터페이스에 정의된 메서드 구현 move(): void { console.log(${this.moveSpeed} 속도로 이동!); } } 이제 Character 클래스는 CharacterInterface의 규칙을 따르게 된다. 만약 인터페이스에 정의된 프로퍼티나 메서드를 하나라도 빼먹으면 에러가 발생한다. 구현 시 주의할 점 (접근 제어자) 인터페이스에 정의된 프로퍼티나 메서드는 클래스에서 구현할 때 무조건 public 이어야 한다. 인터페이스는 기본적으로 "외부에 노출되는 인터페이스(접점)"를 정의하는 것이기 때문에, 이를 private이나 protected로 숨길 수 없다. TypeScript interface CharacterInterface { name: string; } class Character implements CharacterInterface { // ❌ 에러: 인터페이스의 속성은 private일 수 없음 // private name: string; constructor(public name: string) {} // ✅ public은 가능 } > 참고: 인터페이스에 정의되지 않은 추가 필드(private extra 등)는 private이나 protected로 자유롭게 설정할 수 있다. > --- 3. 요약 1. 설계도 역할: 인터페이스를 사용해 클래스가 가져야 할 필드와 메서드의 구조를 정의할 수 있다. 2. implements: 클래스가 특정 인터페이스를 준수하도록 강제하는 키워드다. 3. 규칙: 인터페이스에 정의된 속성은 클래스에서 반드시 구현해야 하며, 접근 제어자는 public 이어야 한다.

TypeScriptStudyClassInterfaceOOP
TypeScript

[TypeScript] 클래스 접근 제어자

November 27, 2025

접근 제어자(Access Modifier) 는 클래스의 특정 필드나 메서드에 접근할 수 있는 범위를 설정하는 기능이다. 자바스크립트(ES6) 클래스에는 없는 타입스크립트만의 고유 기능으로, 객체 지향 프로그래밍의 특징인 은닉화(Encapsulation) 를 돕는다. 타입스크립트에는 다음 3가지의 접근 제어자가 존재한다. 1. public: 모든 범위에서 접근 가능 2. protected: 클래스 내부 + 파생(자식) 클래스 내부에서만 접근 가능 3. private: 오직 클래스 내부에서만 접근 가능 --- 1. public (공공의) public은 말 그대로 '공공의'라는 뜻으로, 어디서든 자유롭게 접근할 수 있다. 접근 제어자를 별도로 명시하지 않으면 기본적으로 public으로 설정된다. TypeScript class Employee { // 필드 (기본적으로 public) name: string; public age: number; // 명시적으로 작성해도 됨 public position: string; constructor(name: string, age: number, position: string) { this.name = name; this.age = age; this.position = position; } work() { console.log("일함"); } } const employee = new Employee("조태민", 25, "developer"); // ✅ 외부에서 자유롭게 접근 및 수정 가능 employee.name = "강두칠"; employee.age = 21; employee.position = "디자이너"; --- 2. private (사적인) private은 오직 해당 클래스 내부에서만 접근할 수 있다. 외부에서는 물론이고, 상속받은 자식 클래스에서도 접근할 수 없다. 가장 엄격한 제어자다. TypeScript class Employee { private name: string; // 🔒 private 설정 public age: number; public position: string; constructor(name: string, age: number, position: string) { this.name = name; this.age = age; this.position = position; } work() { // ✅ 클래스 내부에서는 접근 가능 console.log(${this.name}이 일함); } } const employee = new Employee("조태민", 25, "developer"); // ❌ 오류 발생: 외부에서 접근 불가 // employee.name = "강두칠"; --- 3. protected (보호된) protected는 public과 private의 중간 단계다. 외부에서는 접근할 수 없지만, 클래스 내부와 이를 상속받은 파생 클래스(자식) 에서는 접근할 수 있다. TypeScript class Employee { private name: string; // 나만 쓸 거야 protected age: number; // 자식한테는 물려줄 거야 public position: string; // 다 써도 돼 constructor(name: string, age: number, position: string) { this.name = name; this.age = age; this.position = position; } } // Employee를 상속받은 자식 클래스 class ExecutiveOfficer extends Employee { func() { // this.name; // ❌ 오류 (private은 자식도 접근 불가) this.age; // ✅ 가능 (protected는 자식 접근 허용) } } const employee = new Employee("조태민", 25, "developer"); // ❌ 오류: 외부에서는 protected도 접근 불가 // employee.age = 30; 🔍 접근 범위 한눈에 보기 | 제어자 | 클래스 내부 | 자식 클래스 | 클래스 외부 | | --- | --- | --- | --- | | public | ⭕️ | ⭕️ | ⭕️ | | protected | ⭕️ | ⭕️ | ❌ | | private | ⭕️ | ❌ | ❌ | --- 4. 꿀팁: 생성자 매개변수로 필드 생략하기 타입스크립트에서는 생성자(constructor)의 매개변수에 접근 제어자를 붙여주면, 필드 선언과 초기화 코드를 한 번에 생략할 수 있다. Before: 일반적인 작성법 필드 선언하고, 생성자에서 매개변수 받고, this로 할당하고... 코드가 길다. TypeScript class Employee { private name: string; protected age: number; public position: string; constructor(name: string, age: number, position: string) { this.name = name; this.age = age; this.position = position; } } After: 접근 제어자 활용 (추천 👍) 생성자 매개변수 앞에 public, private, protected 중 하나만 붙이면, 타입스크립트가 알아서 "아, 이거 필드로 만들고 값도 자동으로 넣으라는 거구나" 라고 이해한다. TypeScript class Employee { // 필드 선언 제거! // 초기화 코드(this.xxx = xxx) 제거! constructor( private name: string, protected age: number, public position: string ) {} // 생성자 본문이 비어있어도 됨 work() { console.log(${this.name} 일함); } } 코드가 훨씬 간결해지므로 실무에서 매우 자주 사용하는 패턴이다. --- 요약 1. public: 기본값. 어디서든 접근 가능. 2. private: 내 클래스 안에서만 접근 가능. (가장 엄격) 3. protected: 내 클래스 + 상속받은 자식 클래스에서 접근 가능. 4. 필드 생략: 생성자 매개변수에 접근 제어자를 붙이면 필드 선언과 초기화를 자동으로 처리해 준다.

TypeScriptStudyClassOOPAccessModifier
TypeScript

[TypeScript] 클래스(Class)

November 27, 2025

자바스크립트(ES6)의 클래스 문법에 타입스크립트만의 타입 시스템이 더해지면 어떻게 될까? 필드 선언부터 상속 시 주의할 점까지, 타입스크립트에서의 클래스 사용법을 정리해 본다. 1. 클래스 필드 선언 타입스크립트에서 클래스를 만들 때는 필드(Field) 를 선언할 때 반드시 타입을 함께 정의해야 한다. TypeScript class Employee { // 필드 name: string = ""; age: number = 0; position: string = ""; // 메서드 work() { console.log("일함"); } } 주의할 점 1. 타입 주석 필수: 타입을 정의하지 않으면 암시적 any 타입으로 추론된다. strict 모드(엄격한 타입 검사)에서는 이것이 오류로 간주된다. 2. 초기값 설정: 생성자(constructor)에서 값을 초기화하지 않는다면, 필드 선언 시 기본값을 할당해 줘야 한다. 그렇지 않으면 "초기화되지 않았다"는 오류가 발생한다. --- 2. 생성자(Constructor)와 선택적 프로퍼티 생성자를 이용해 인스턴스 생성 시점에 필드 값을 초기화할 수 있다. 생성자에서 할당이 확실하게 이루어진다면, 필드 선언부에서 초기값을 생략해도 된다. TypeScript class Employee { // 필드 name: string; age: number; position: string; // 생성자 constructor(name: string, age: number, position: string) { this.name = name; this.age = age; this.position = position; } work() { console.log("일함"); } } 선택적 프로퍼티 (Optional Property) 특정 필드가 있어도 되고 없어도 되는 경우라면, 필드 이름 뒤에 물음표(?)를 붙여 선택적 프로퍼티로 만들 수 있다. TypeScript class Employee { // ... position?: string; // 선택적 프로퍼티 constructor(name: string, age: number, position: string) { // ... this.position = position; } } --- 3. 클래스는 타입이다 타입스크립트의 클래스는 값이자 동시에 타입으로도 사용된다. 즉, 클래스로 만든 인스턴스뿐만 아니라, 그 클래스의 형태(구조)를 가진 객체라면 해당 클래스 타입으로 정의할 수 있다. TypeScript class Employee { // ... (위와 동일) } // Employee 클래스를 타입으로 사용 const employeeC: Employee = { name: "조태민", age: 25, position: "Developer", work() { console.log("일하는 중"); }, }; 변수 employeeC는 Employee 클래스의 인스턴스(new Employee(...))가 아니지만, Employee가 가진 필드와 메서드를 모두 가지고 있으므로 Employee 타입으로 인정된다. (구조적 타이핑) --- 4. 상속 (Inheritance) extends 키워드를 사용해 클래스를 상속받을 수 있다. 이때 생성자(constructor)를 다룰 때 주의해야 할 규칙이 있다. TypeScript // Employee를 상속받는 ExecutiveOfficer 클래스 class ExecutiveOfficer extends Employee { officeNumber: number; constructor( name: string, age: number, position: string, officeNumber: number ) { super(name, age, position); // ✅ 반드시 최상단에서 호출! this.officeNumber = officeNumber; } } 상속 시 주의사항 (super) 파생 클래스(자식)에서 생성자를 정의했다면, 반드시 super() 메서드를 호출해 부모 클래스의 생성자를 실행해야 한다. 또한 super() 호출은 this에 접근하기 전, 생성자 내부의 최상단에 위치해야 한다. --- 요약 1. 필드 선언: 필드의 타입과 초기값을 명시해야 안전하다. 2. 생성자: 필드 초기화의 역할을 하며, ?를 써서 선택적 필드를 만들 수 있다. 3. 타입으로서의 클래스: 클래스 이름은 그 자체로 타입으로 사용될 수 있다. 4. 상속: extends를 사용하며, 자식 클래스 생성자에서는 super()를 가장 먼저 호출해야 한다.

TypeScriptStudyClassOOP
TypeScript

[TypeScript] 인터페이스 선언 합침 (Declaration Merging)

November 27, 2025

타입스크립트에서 타입 별칭(Type Alias) 은 동일한 스코프 내에서 같은 이름으로 중복 선언하는 것이 불가능하다. TypeScript type Person = { name: string; }; // ❌ 에러 발생: 중복된 식별자 'Person' // type Person = { // age: number; // }; 하지만 인터페이스(Interface) 는 가능하다. 심지어 에러가 나지 않을 뿐만 아니라, 알아서 하나로 합쳐진다. 1. 선언 합침이란? 동일한 이름으로 정의된 여러 개의 인터페이스가 컴파일 시점에 자동으로 하나의 인터페이스로 합쳐지는 기능을 말한다. TypeScript // 첫 번째 선언 interface Person { name: string; } // 두 번째 선언 (에러 없음 ✅) interface Person { age: number; } 위 코드는 내부적으로 다음과 같이 하나의 인터페이스로 합쳐진다. TypeScript // 결과적으로 이렇게 됨 interface Person { name: string; age: number; } 따라서 Person 타입의 변수를 생성할 때는 두 인터페이스에 정의된 모든 프로퍼티를 구현해야 한다. TypeScript const person: Person = { name: "조태민", age: 25, }; 이 기능은 주로 외부 라이브러리의 기존 타입 정의에 내가 필요한 속성을 추가하고 싶을 때(예: Window 객체 확장 등) 유용하게 사용된다. --- 2. 주의할 점: 프로퍼티 충돌 인터페이스가 합쳐질 때, 만약 동일한 이름의 프로퍼티가 양쪽에 존재한다면 어떻게 될까? 1. 타입이 같은 경우: 문제 없다. (그냥 합쳐짐) 2. 타입이 다른 경우: ❌ 에러가 발생한다 (충돌). TypeScript interface Person { name: string; } interface Person { // ❌ 에러 발생! // 후속 속성 선언의 타입은 반드시 기존 선언의 타입과 같아야 한다. // 'string' 형식이어야 하는데 'number' 형식이다. name: number; age: number; } 첫 번째 Person에서는 name을 string으로, 두 번째 Person에서는 number로 정의했다. 이렇게 동일한 프로퍼티를 서로 다른 타입으로 정의하면 충돌로 간주되어 선언 합침이 허용되지 않는다. --- 요약 1. 타입 별칭은 중복 선언이 불가능하지만, 인터페이스는 가능하다. 2. 선언 합침: 같은 이름의 인터페이스는 자동으로 하나로 합쳐진다. 3. 충돌 주의: 합쳐지는 과정에서 동일한 프로퍼티 이름인데 타입이 다르면 에러가 발생한다.

TypeScriptStudyInterfaceDeclarationMerging
TypeScript

[TypeScript] 인터페이스 확장 (Interface Extension)

November 27, 2025

타입스크립트에서 인터페이스 확장이란, 하나의 인터페이스를 다른 인터페이스들이 상속받아 중복된 프로퍼티를 다시 정의하지 않도록 도와주는 문법이다. 객체지향 프로그래밍의 '상속' 개념과 매우 유사하다. 1. 왜 확장이 필요한가? (중복의 문제) 만약 확 기능을 사용하지 않고 여러 동물의 타입을 정의한다면 어떻게 될까? TypeScript interface Animal { name: string; age: number; } interface Dog { name: string; // 중복 age: number; // 중복 isBark: boolean; } interface Cat { name: string; // 중복 age: number; // 중복 isScratch: boolean; } interface Chicken { name: string; // 중복 age: number; // 중복 isFly: boolean; } Dog, Cat, Chicken 모두 Animal의 특징(name, age)을 가지고 있다. 이렇게 되면 코드가 중복될 뿐만 아니라, 유지보수가 매우 힘들어진다. 만약 Animal의 age를 ages로 수정해야 한다면? 나머지 3개의 인터페이스를 일일이 찾아다니며 모두 수정해야 하는 대참사가 일어난다. --- 2. 인터페이스 확장 사용하기 (extends) 이럴 때 extends 키워드를 사용하면 효율적으로 타입을 정의할 수 있다. TypeScript interface Animal { name: string; color: string; } // Animal을 확장(상속)받음 interface Dog extends Animal { breed: string; } interface Cat extends Animal { isScratch: boolean; } interface Chicken extends Animal { isFly: boolean; } 이제 Dog, Cat, Chicken은 Animal에 정의된 name과 color 프로퍼티를 자동으로 갖게 된다. TypeScript const dog: Dog = { name: "춘식이", // Animal에서 물려받음 color: "brown", // Animal에서 물려받음 breed: "리트리버", // Dog만의 프로퍼티 }; 이때 상속을 해주는 Animal은 슈퍼 타입(부모), 상속을 받는 Dog는 서브 타입(자식) 이 된다. --- 3. 프로퍼티 재정의 (Overriding) 확장과 동시에 부모에게 물려받은 프로퍼티의 타입을 재정의할 수도 있다. TypeScript interface Animal { name: string; color: string; } interface Dog extends Animal { name: "도로롱"; // string -> "도로롱" (Literal Type)으로 재정의 breed: string; } ⚠️ 주의할 점: 타입 호환성 규칙 프로퍼티를 재정의할 때는 반드시 원본 타입의 서브 타입으로만 재정의할 수 있다. TypeScript interface Animal { name: string; color: string; } interface Dog extends Animal { name: number; // ❌ 에러 발생! breed: string; } 왜 안 될까?Dog가 Animal을 확장한다는 것은 Dog가 Animal의 서브 타입(자식)이 된다는 뜻이다. 하지만 name을 number로 바꿔버리면, Dog는 더 이상 Animal(name이 string인 집합)에 포함될 수 없게 된다. 따라서 원본 타입의 범위를 벗어나는 재정의는 불가능하다. --- 4. 타입 별칭(Type Alias) 확장하기 재미있는 점은 인터페이스가 인터페이스뿐만 아니라 타입 별칭으로 정의된 객체도 확장할 수 있다는 것이다. TypeScript type Animal = { name: string; color: string; }; // 타입 별칭을 인터페이스가 확장 interface Dog extends Animal { breed: string; } --- 5. 다중 확장 (Multiple Extension) 여러 개의 인터페이스를 동시에 확장하는 것도 가능하다. 콤마(,)를 사용하면 된다. TypeScript interface Dog { name: string; isBark: boolean; } interface Cat { name: string; isScratch: boolean; } // 개이면서 동시에 고양이인 혼종(?) 타입 interface DogCat extends Dog, Cat {} const dogCat: DogCat = { name: "개냥이", isBark: true, isScratch: true, }; --- 요약 1. 인터페이스 확장 (extends): 중복 코드를 줄이고 유지보수성을 높여준다. 2. 프로퍼티 재정의: 가능하지만, 원본 타입의 서브 타입 범위 내에서만 가능하다. 3. 유연성: 타입 별칭(type)도 확장할 수 있으며, 여러 인터페이스를 한 번에 확장(다중 확장)할 수도 있다.

TypeScriptStudyInterfaceExtends
TypeScript

[TypeScript] 인터페이스(Interface)

November 27, 2025

타입스크립트에는 객체의 타입을 정의하는 방법이 크게 두 가지가 있다. 하나는 앞서 배운 타입 별칭(Type Alias) 이고, 다른 하나는 이번에 다룰 인터페이스(Interface) 이다. 인터페이스는 타입에 이름을 지어주는 또 다른 문법으로, 특히 객체의 구조를 정의하는 데 특화되어 있다. 1. 기본 문법 인터페이스는 interface 키워드를 사용하여 정의한다. 타입 별칭과 문법만 조금 다를 뿐, 기본적인 기능은 거의 같다. TypeScript // 인터페이스 정의 interface Person { name: string; age: number; } // 변수에 타입 주석으로 사용 const person: Person = { name: "조태민", age: 25, }; --- 2. 프로퍼티 설정 객체 리터럴 타입에서 배웠던 선택적 프로퍼티나 읽기 전용 프로퍼티도 동일하게 사용할 수 있다. 2-1. 선택적 프로퍼티 (Optional Property) 프로퍼티 이름 뒤에 ?를 붙여서, 해당 속성이 없어도 에러가 나지 않게 설정할 수 있다. TypeScript interface Person { name: string; age?: number; // 있어도 되고 없어도 됨 } const person: Person = { name: "조태민", // age는 생략 가능 }; 2-2. 읽기 전용 프로퍼티 (Readonly Property) 프로퍼티 이름 앞에 readonly를 붙여서, 초기화 이후 값을 수정할 수 없게 만든다. TypeScript interface Person { readonly name: string; // 읽기 전용 age?: number; } const person: Person = { name: "조태민", age: 25, }; // person.name = "홍길동"; // ❌ 에러 발생 (수정 불가) --- 3. 메서드 타입 정의와 오버로딩 인터페이스 내부에서 메서드의 타입을 정의하는 방법은 두 가지가 있다. 어떤 방식을 쓰느냐에 따라 오버로딩 가능 여부가 달라지므로 주의해야 한다. 3-1. 함수 타입 표현식 (화살표 함수 형태) 가장 일반적인 형태지만, 메서드 오버로딩을 구현할 수 없다는 단점이 있다. TypeScript interface Person { sayHi: () => void; // sayHi: (a: number) => void; // ❌ 오버로딩 불가능 (에러 발생) } 3-2. 호출 시그니처 (Call Signature) 메서드 이름 뒤에 괄호를 여는 방식이다. 이 방식을 사용하면 메서드 오버로딩을 구현할 수 있다. TypeScript interface Person { sayHi(): void; sayHi(a: number): void; // ✅ 오버로딩 가능 sayHi(a: number, b: number): void; // ✅ 오버로딩 가능 } --- 4. 하이브리드 타입 (Hybrid Type) 자바스크립트에서는 함수도 객체이므로, 함수이면서 동시에 객체(프로퍼티를 가짐) 인 형태를 만들 수 있다. 인터페이스로도 이를 정의할 수 있다. TypeScript interface Func2 { (a: number): string; // 호출 시그니처 (함수로서의 역할) b: boolean; // 프로퍼티 (객체로서의 역할) } const func: Func2 = (a) => "hello"; func.b = true; --- 5. 주의할 점 (타입 별칭과의 차이) 인터페이스는 타입 별칭(type)과 거의 비슷하게 동작하지만, Union(|)이나 Intersection(&) 타입을 직접 정의할 수는 없다. TypeScript // 타입 별칭은 가능 type Type1 = number | string; // 인터페이스는 불가능 (문법 오류) // interface Person { ... } | number // ❌ 만약 인터페이스와 유니온/인터섹션을 함께 사용하고 싶다면, 타입 별칭을 이용해 합치거나 타입 주석에서 직접 사용해야 한다. TypeScript interface Person { name: string; age: number; } // 타입 별칭을 이용해 결합 type Type1 = number | string | Person; // 변수 선언 시 결합 const person: Person & string = { name: "조태민", age: 25, // ... string 메서드 등 }; --- 요약 1. 인터페이스: 객체의 구조를 정의하는 문법이다 (interface Name { ... }). 2. 프로퍼티: 선택적(?), 읽기 전용(readonly) 설정이 가능하다. 3. 메서드: - func: () => void (함수 타입 표현식) -> 오버로딩 불가 ❌ - func(): void (호출 시그니처) -> 오버로딩 가능 ✅ 4. 한계: 인터페이스 자체적으로 Union이나 Intersection을 생성할 수 없다 (타입 별칭과 조합하여 해결).

TypeScriptStudyInterfaceObject
TypeScript

[TypeScript] 사용자 정의 타입 가드 (User-Defined Type Guard)

November 27, 2025

타입스크립트에서는 typeof나 instanceof 같은 연산자로 타입을 좁히기 애매하거나, 더 복잡한 로직으로 타입을 구분해야 할 때가 있다. 이때 사용할 수 있는 것이 사용자 정의 타입 가드다. 쉽게 말해 "참 또는 거짓을 반환하는 함수를 이용해 우리 입맛대로 타입 가드를 만드는 문법" 이다. 1. 기존 방식(in 연산자)의 문제점 먼저 Dog와 Cat 타입을 정의하고, 이를 유니온으로 묶은 Animal 타입을 예로 들어보자. TypeScript type Dog = { name: string; isBark: boolean; }; type Cat = { name: string; isScratch: boolean; }; type Animal = Dog | Cat; 기존에 배운 in 연산자를 사용하면 다음과 같이 타입을 좁힐 수 있다. TypeScript function warning(animal: Animal) { if ("isBark" in animal) { // Dog 타입으로 추론됨 console.log(animal.isBark ? "짖습니다" : "안짖어요"); } else if ("isScratch" in animal) { // Cat 타입으로 추론됨 console.log(animal.isScratch ? "할큅니다" : "안할퀴어요"); } } 이 코드는 잘 동작하지만 한 가지 단점이 있다. 만약 Dog 타입의 isBark 프로퍼티 이름이 isBarked로 바뀐다면? 타입 정의만 바뀌고 warning 함수 안의 문자열 "isBark"는 그대로 남아있기 때문에, 타입 가드가 제대로 동작하지 않게 된다. 즉, 유지보수가 어렵고 실수를 유발할 수 있다. 2. 사용자 정의 타입 가드 구현 (is 키워드) 이럴 때 함수를 따로 만들어서 타입을 확실하게 검사해주면 좋다. 이때 반환 타입에 사용하는 것이 parameter is Type 문법이다. TypeScript // Dog 타입인지 확인하는 타입 가드 function isDog(animal: Animal): animal is Dog { return (animal as Dog).isBark !== undefined; } // Cat 타입인지 확인하는 타입 가드 function isCat(animal: Animal): animal is Cat { return (animal as Cat).isScratch !== undefined; } 핵심 포인트: animal is Dog 함수의 반환 타입을 단순히 boolean으로 적는 것이 아니라, animal is Dog라고 적어야 한다. - 이 함수가 true를 반환하면 -> 인자로 받은 animal은 Dog 타입임이 보장된다는 의미다. - 타입스크립트 컴파일러에게 "내가 검사해봤는데 이거 Dog 맞으니까 믿고 좁혀!"라고 알려주는 것이다. 3. 활용하기 이제 만든 커스텀 타입 가드 함수를 if문의 조건으로 사용하면 된다. TypeScript function warning(animal: Animal) { if (isDog(animal)) { // 여기 들어왔다는 건 isDog가 true라는 뜻 // 즉, animal은 Dog 타입으로 좁혀짐 console.log(animal.isBark ? "짖습니다" : "안짖어요"); } else if (isCat(animal)) { // 여기는 Cat 타입 console.log(animal.isScratch ? "할큅니다" : "안할퀴어요"); } } 이제 프로퍼티 이름이 바뀌더라도 isDog 함수 내부만 수정하면 되고, warning 함수는 건드릴 필요가 없다. 코드가 훨씬 읽기 좋아지고 안전해졌다. --- 요약 1. 사용자 정의 타입 가드: 함수를 이용해 타입을 좁히는 방식이다. 2. is 키워드: 반환 타입에 param is Type 형식을 사용해야 한다. 3. 장점: in 연산자처럼 문자열에 의존하지 않으므로, 복잡한 타입 검사 로직을 캡슐화하고 재사용성을 높일 수 있다.

TypeScriptStudyTypeGuardTypeNarrowing
TypeScript

[TypeScript] 함수 오버로딩 (Function Overloading)

November 27, 2025

타입스크립트에서는 하나의 함수가 매개변수의 개수나 타입에 따라 다르게 동작하도록 만들 수 있다. 이를 함수 오버로딩(Function Overloading) 이라고 한다. C이나 Java 같은 언어에서는 흔한 기능이지만, 자바스크립트는 원래 이를 지원하지 않는다. 하지만 타입스크립트에서는 오버로드 시그니처를 통해 이 기능을 흉내 낼 수 있다. 1. 함수 오버로딩의 구조 함수 오버로딩을 구현하려면 크게 두 단계가 필요하다. 1. 오버로드 시그니처 (Overload Signature): 함수의 껍데기. 호출 가능한 버전을 정의한다. 2. 구현 시그니처 (Implementation Signature): 함수의 알맹이. 실제 로직을 구현한다. 예시 시나리오 하나의 함수 func를 만드는데, 다음 두 가지 상황을 모두 처리하고 싶다고 가정해보자. - Ver 1 (매개변수 1개): 숫자를 하나 받으면 x 20 한 값을 출력. - Ver 2 (매개변수 3개): 숫자를 세 개 받으면 모두 더한 값을 출력. --- 2. 오버로드 시그니처 (선언부) 먼저 함수가 어떤 형태의 매개변수를 받을 수 있는지 선언만 해둔다. 구현부({}) 없이 작성한다. TypeScript // 버전 1: 매개변수 1개 function func(a: number): void; // 버전 2: 매개변수 3개 function func(a: number, b: number, c: number): void; 이제 컴파일러는 func 함수가 인자를 1개 받거나, 3개 받는 경우만 허용한다는 것을 알게 된다. --- 3. 구현 시그니처 (구현부) 이제 실제로 함수가 어떻게 동작할지 코드를 작성해야 한다. 이때 중요한 점은 구현 시그니처의 매개변수는 위에서 정의한 모든 오버로드 시그니처를 커버할 수 있어야 한다는 것이다. TypeScript // 실제 구현부 function func(a: number, b?: number, c?: number) { if (typeof b === "number" && typeof c === "number") { // 매개변수가 3개 들어온 경우 (Ver 2) console.log(a + b + c); } else { // 매개변수가 1개 들어온 경우 (Ver 1) console.log(a 20); } } 💡 왜 b와 c가 선택적 매개변수(?)여야 할까? 첫 번째 오버로드 시그니처(func(a: number))는 b와 c를 받지 않는다. 따라서 구현부에서 b와 c를 필수값으로 정의하면 첫 번째 버전과 호환되지 않아 에러가 발생한다. 모든 버전을 포용하기 위해 유연하게 정의해야 한다. --- 4. 함수 호출 및 결과 이제 정의한 함수를 사용해보자. TypeScript func(1); // ✅ 버전 1 호출 -> 20 출력 func(1, 2, 3); // ✅ 버전 3 호출 -> 6 출력 // func(1, 2); // ❌ 에러 발생! func(1, 2)는 인자를 2개 넘겼다. 우리는 인자 1개짜리 버전과 3개짜리 버전만 만들었지, 2개짜리 오버로드 시그니처는 만들지 않았다. 따라서 타입스크립트는 이를 에러로 잡는다. --- 요약 1. 함수 오버로딩: 매개변수의 형태에 따라 다르게 동작하는 함수를 만드는 문법. 2. 오버로드 시그니처: 함수의 매개변수와 반환값 타입만 정의한 선언부. (여러 개 가능) 3. 구현 시그니처: 실제 로직이 있는 부분. 모든 오버로드 시그니처와 호환되어야 하므로 매개변수 타입을 유연하게(Optional 등) 설정해야 한다.

TypeScriptStudyFunctionOverloading
TypeScript

[TypeScript] 함수 타입의 호환성

November 27, 2025

타입스크립트에서 함수 타입의 호환성이란, 특정 함수 타입을 다른 함수 타입으로 취급해도 괜찮은지(할당 가능한지)를 판단하는 것을 말한다. 함수 타입의 호환성은 다음 2가지 기준으로 체크한다. 1. 반환값(Return) 의 타입이 호환되는가? 2. 매개변수(Parameter) 의 타입이 호환되는가? 하나씩 자세히 살펴보자. 1. 기준 1: 반환값 타입이 호환되는가? 반환값은 우리가 아는 일반적인 객체 타입의 호환성 원칙(공변성)과 같다. 즉, A의 반환값이 B의 반환값의 서브 타입(자식)이어야 호환된다. (업캐스팅 가능) TypeScript type A = () => number; type B = () => 10; let a: A = () => 10; let b: B = () => 10; a = b; // ✅ OK (number는 10을 포함함) // b = a; // ❌ NO (10은 number 전체를 포함하지 못함) 변수 a는 number를 반환하기를 기대하고, b 함수는 10을 반환한다. 10은 number에 속하므로 호환된다. 반대는 성립하지 않는다. --- 2. 기준 2: 매개변수의 타입이 호환되는가? 매개변수의 호환성은 매개변수의 개수가 같은지 다른지에 따라 판단 기준이 달라진다. 2-1. 매개변수의 개수가 같을 때 (반공변성) 이 부분이 가장 헷갈리는 부분이다. 결론부터 말하면 반환값과는 반대로, 할당하려는 함수의 매개변수 타입이 더 상위(슈퍼) 타입이어야 호환된다. 일반적인 객체 호환성과 반대되는 개념이라 반공변성(Contravariance) 이라고도 부른다. TypeScript type C = (value: number) => void; type D = (value: 10) => void; let c: C = (value) => {}; let d: D = (value) => {}; // c = d; // ❌ NO (number <-- 10 : 업캐스팅 같지만 함수 매개변수에서는 안 됨) d = c; // ✅ OK (10 --> number : 다운캐스팅 같지만 함수 매개변수에서는 됨) 왜 반대일까? (객체 예시로 이해하기) Animal(부모)과 Dog(자식) 타입을 예로 들어보자. TypeScript type Animal = { name: string }; type Dog = { name: string; color: string }; let animalFunc = (animal: Animal) => { console.log(animal.name); }; let dogFunc = (dog: Dog) => { console.log(dog.name); console.log(dog.color); }; // animalFunc = dogFunc; // ❌ 불가능 dogFunc = animalFunc; // ✅ 가능 - 불가능한 이유 (animalFunc = dogFunc): animalFunc는 Animal 타입의 인수를 받는다. 만약 여기에 dogFunc를 할당해버리면, dogFunc는 내부적으로 dog.color를 찾으려 할 것이다. 하지만 Animal 타입에는 color가 없으므로 런타임 에러가 발생할 수 있다. - 가능한 이유 (dogFunc = animalFunc): dogFunc는 Dog 타입의 인수를 받는다. 여기에 animalFunc를 할당하면, animalFunc는 들어온 인수에서 name만 사용한다. Dog 타입에는 name이 보장되어 있으므로 안전하다. > 결론: 매개변수 개수가 같을 때는 매개변수 타입이 더 넓은(슈퍼 타입인) 함수를 대입해야 안전하다. > --- 2-2. 매개변수의 개수가 다를 때 매개변수의 개수가 다를 때는 비교적 단순하다. 매개변수의 개수가 더 적은 함수는 호환된다. (단, 타입은 같아야 함) TypeScript type Func1 = (a: number, b: number) => void; type Func2 = (a: number) => void; let func1: Func1 = (a, b) => {}; let func2: Func2 = (a) => {}; func1 = func2; // ✅ OK (인자 2개 받는 자리에 1개 받는 함수 넣기 가능) // func2 = func1; // ❌ NO (인자 1개 받는 자리에 2개 필요한 함수 넣기 불가) 자바스크립트/타입스크립트에서는 함수에 정의된 매개변수보다 더 많은 인자를 넘겨도, 함수가 알아서 무시하고 실행되기 때문에 이는 안전한 동작으로 간주된다. --- 요약 1. 반환값: 일반적인 객체와 똑같다. (자식 타입을 부모 타입에 할당 가능 - 업캐스팅) 2. 매개변수 (개수 같음): 일반적인 객체와 반대다. (부모 타입을 자식 타입에 할당 가능 - 반공변성) 3. 매개변수 (개수 다름): 매개변수가 더 적은 쪽을 할당할 수 있다.

TypeScriptStudyFunctionTypeCompatibility
TypeScript

[TypeScript] 함수 타입 표현식과 호출 시그니처

November 27, 2025

타입스크립트에서는 함수를 선언할 때 매개변수 옆에 일일이 타입을 적는 방식 외에도, 함수의 타입만 따로 정의해두고 재사용하는 방법을 제공한다. 이를 가능하게 하는 두 가지 방법, 함수 타입 표현식과 호출 시그니처에 대해 알아본다. 1. 함수 타입 표현식 (Function Type Expression) 함수 타입 표현식은 타입 별칭(type)을 사용하여 함수의 타입을 별도로 정의하는 문법이다. 1-1. 기본 문법 화살표 함수와 비슷한 형태를 띤다. (매개변수 타입) => 반환값 타입 형식으로 작성한다. TypeScript // 함수 타입 정의 type Add = (a: number, b: number) => number; // 함수 구현 (타입 별칭 사용) const add: Add = (a, b) => a + b; 이렇게 하면 실제 함수 구현부(const add ...)에서는 매개변수의 타입을 생략해도 된다. 이미 Add 타입에서 정의했기 때문에 타입스크립트가 타입을 추론할 수 있기 때문이다. 1-2. 사용하는 이유 (재사용성) 함수 타입 표현식은 여러 함수가 동일한 타입을 가질 때 매우 유용하다. 예를 들어 계산기 기능을 만든다고 가정해 보자. 더하기, 빼기, 곱하기, 나누기 모두 (숫자, 숫자) => 숫자 형태를 가진다. Before: 일일이 정의하는 경우 TypeScript const add = (a: number, b: number) => a + b; const sub = (a: number, b: number) => a - b; const multiply = (a: number, b: number) => a b; const divide = (a: number, b: number) => a / b; 코드가 길어지고 중복이 많다. After: 함수 타입 표현식 사용 TypeScript // 공통 타입 정의 type Operation = (a: number, b: number) => number; const add: Operation = (a, b) => a + b; const sub: Operation = (a, b) => a - b; const multiply: Operation = (a, b) => a b; const divide: Operation = (a, b) => a / b; 코드가 훨씬 간결해졌다. 만약 나중에 타입 정의를 수정해야 한다면 Operation 타입 하나만 수정하면 되므로 유지보수성도 좋아진다. > 참고: 타입 별칭 없이 인라인으로 사용할 수도 있다. > > > const add: (a: number, b: number) => number = (a, b) => a + b; > --- 2. 호출 시그니처 (Call Signature) 호출 시그니처는 함수 타입 표현식과 동일한 기능을 하지만, 객체 리터럴 문법을 사용하여 정의하는 방식이다. 2-1. 기본 문법 중괄호 {}를 열고, 그 안에 (매개변수): 반환값 형태로 작성한다. (화살표 => 대신 콜론 :을 사용함에 주의하자) TypeScript type Operation2 = { (a: number, b: number): number; }; const add2: Operation2 = (a, b) => a + b; const sub2: Operation2 = (a, b) => a - b; const multiply2: Operation2 = (a, b) => a b; const divide2: Operation2 = (a, b) => a / b; 2-2. 왜 객체처럼 정의할까? (하이브리드 타입) 자바스크립트에서는 함수도 객체이다. 즉, 함수에 프로퍼티를 추가해서 사용할 수 있다. 호출 시그니처를 사용하면 함수이면서 동시에 일반 객체처럼 프로퍼티를 가지는 타입을 정의할 수 있다. 이를 하이브리드 타입이라고 부른다. TypeScript type Operation2 = { (a: number, b: number): number; // 1. 함수로서 호출 가능 name: string; // 2. 일반 객체 프로퍼티 보유 }; const add2: Operation2 = (a, b) => a + b; // 하이브리드 타입 구현을 위해 프로퍼티 추가 add2.name = "Add Function"; console.log(add2(1, 2)); // 3 (함수로 사용) console.log(add2.name); // "Add Function" (객체로 사용) --- 요약 1. 함수 타입 표현식: type Name = (params) => returnType 형태로, 함수의 타입을 간결하게 정의하고 재사용할 때 사용한다. 2. 호출 시그니처: type Name = { (params): returnType } 형태로, 객체 문법을 사용해 정의한다. 3. 하이브리드 타입: 호출 시그니처를 사용하면 함수 기능과 객체 프로퍼티를 동시에 가지는 타입을 정의할 수 있다.

TypeScriptStudyFunctionTypeDefinition
TypeScript

[TypeScript] 함수(Function) 타입 정의하기

November 27, 2025

자바스크립트에서 함수를 설명할 때 가장 중요한 것은 "어떤 매개변수를 받아서, 어떤 값을 반환하는가" 이다. 타입스크립트도 마찬가지다. 단지 그 설명에 타입만 추가하면 된다. 1. 기본적인 함수 타입 정의 함수의 타입은 매개변수의 타입과 반환값의 타입으로 결정된다. 1-1. 함수 선언식 매개변수 뒤에 타입을 적고, 소괄호 뒤에 반환값의 타입을 적는다. TypeScript function func(a: number, b: number): number { return a + b; } 💡 반환값 타입 추론 타입스크립트는 return문을 보고 반환값의 타입을 자동으로 추론할 수 있다. 따라서 반환 타입은 생략해도 무방하다. TypeScript function func(a: number, b: number) { return a + b; // 자동으로 number 반환으로 추론됨 } 1-2. 화살표 함수 화살표 함수도 선언식과 동일한 방식으로 타입을 정의한다. TypeScript const add = (a: number, b: number): number => a + b; // 반환값 타입 생략 가능 const add2 = (a: number, b: number) => a + b; --- 2. 매개변수 다루기 함수의 매개변수를 정의할 때 사용할 수 있는 유용한 기능들이다. 2-1. 매개변수 기본값 (Default Parameter) 매개변수에 기본값이 설정되어 있다면, 타입스크립트가 해당 기본값을 기준으로 타입을 자동으로 추론한다. TypeScript function introduce(name = "조태민") { console.log(name : ${name}); } // introduce(1); // ❌ 오류: string 타입에 number 할당 불가 만약 기본값과 다른 타입으로 명시하거나, 다른 타입의 값을 인수로 전달하면 에러가 발생한다. 2-2. 선택적 매개변수 (Optional Parameter) 매개변수가 있어도 되고 없어도 되는 경우, 변수명 뒤에 물음표(?)를 붙여 선택적 매개변수로 만들 수 있다. TypeScript function introduce(name = "조태민", tall?: number) { console.log(name : ${name}); // tall은 number | undefined 타입이 됨 if (typeof tall === "number") { console.log(tall : ${tall + 10}); } } introduce("조태민", 178); // ✅ introduce("조태민"); // ✅ (tall 생략 가능) 주의할 점: 1. 선택적 매개변수의 타입은 자동으로 undefined와 유니온 된 타입(Type | undefined)이 된다. 따라서 사용 시 타입 좁히기가 필요할 수 있다. 2. 선택적 매개변수는 필수 매개변수보다 앞에 올 수 없다. 항상 뒤에 배치해야 한다. TypeScript // ❌ 오류 발생: 필수 매개변수(age)는 선택적 매개변수(tall) 뒤에 올 수 없음 // function introduce(name = "조태민", tall?: number, age: number) {} --- 3. 나머지 매개변수 (Rest Parameter) 가변적인 개수의 인수를 배열 형태로 받을 때 사용하는 나머지 매개변수(...rest)의 타입 정의 방법이다. 3-1. 배열로 정의하기 일반적으로는 특정 타입의 배열(Type[])로 정의한다. TypeScript function getSum(...rest: number[]) { let sum = 0; rest.forEach((it) => (sum += it)); return sum; } getSum(1, 2, 3); // [1, 2, 3]으로 전달됨 3-2. 튜플로 정의하기 (개수 제한) 만약 인수의 개수를 정확히 제한하고 싶다면 튜플 타입을 사용할 수 있다. TypeScript // 정확히 숫자 3개만 받을 수 있음 function getSum(...rest: [number, number, number]) { let sum = 0; rest.forEach((it) => (sum += it)); return sum; } getSum(1, 2, 3); // ✅ // getSum(1, 2, 3, 4); // ❌ 오류: 3개만 허용됨 --- 요약 1. 기본 정의: 매개변수와 반환값의 타입을 명시한다. (반환값은 추론 가능) 2. 기본값: param = value 형태이며, 타입이 자동 추론된다. 3. 선택적 매개변수: param?: type 형태이며, 필수 매개변수 뒤에 와야 한다. 4. 나머지 매개변수: ...rest: type[] 형태이며, 튜플을 쓰면 개수 제한이 가능하다.

TypeScriptStudyFunction기초문법
TypeScript

[TypeScript] 서로소 유니온 타입 (Discriminated Union)

November 27, 2025

이번에는 타입 좁히기를 훨씬 더 직관적이고 안전하게 할 수 있는 서로소 유니온 타입에 대해 알아본다. 다른 말로는 구별된 유니온(Discriminated Union) 또는 태그된 유니온(Tagged Union) 이라고도 부른다. 1. 서로소 유니온 타입이란? 서로소 유니온 타입은 교집합이 없는 타입들, 즉 서로소 관계에 있는 타입들을 모아 만든 유니온 타입을 말한다. 기본적으로 문자열이나 숫자 리터럴 타입을 사용하여 각 타입을 명확히 구분할 수 있는 '태그(Tag)' 를 붙여서 만든다. 위 그림처럼 각 타입(Admin, Member, Guest)이 서로 겹치는 부분이 전혀 없게 만드는 것이 핵심이다. --- 2. 왜 필요한가? (문제 상황) 회원 관리 프로그램에서 Admin, Member, Guest 세 가지 역할을 구분해야 한다고 가정해 보자. TypeScript type Admin = { name: string; kickCount: number; // 강퇴 횟수 }; type Member = { name: string; point: number; // 포인트 }; type Guest = { name: string; visitCount: number; // 방문 횟수 }; type User = Admin | Member | Guest; 이 상태에서 login 함수를 만들어 각 역할에 맞는 로그를 출력하려면 어떻게 해야 할까? 일반적인 방법으로는 in 연산자를 사용해 특정 프로퍼티가 있는지 확인해야 한다 . TypeScript function login(user: User) { if ("kickCount" in user) { // Admin console.log(${user.name}님 현재까지 ${user.kickCount}명 추방했습니다); } else if ("point" in user) { // Member console.log(${user.name}님 현재까지 ${user.point} 모았습니다); } else { // Guest console.log(${user.name}님 현재까지 ${user.visitCount}번 오셨습니다); } } 문제점 - 직관적이지 않음: 코드를 읽을 때 "kickCount"가 있으면 Admin이구나라고 한 번 더 생각해야 한다. - 복잡함: 타입이 늘어날수록 조건식이 복잡해지고 파악하기 어렵다 . --- 3. 해결책: 태그(Tag) 달기 이 문제를 해결하기 위해 각 타입에 고유한 리터럴 타입의 프로퍼티(tag) 를 추가한다. 이것이 서로소 유니온 타입의 핵심이다. TypeScript type Admin = { tag: "ADMIN"; // 태그 추가 name: string; kickCount: number; }; type Member = { tag: "MEMBER"; // 태그 추가 name: string; point: number; }; type Guest = { tag: "GUEST"; // 태그 추가 name: string; visitCount: number; }; type User = Admin | Member | Guest; 이제 각 타입은 tag라는 공통 프로퍼티를 가지지만, 그 값은 "ADMIN", "MEMBER", "GUEST"로 모두 다르다 . 즉, 서로 교집합이 없는 완벽한 서로소 관계가 되었다. --- 4. 더 직관적인 타입 좁히기 이제 in 연산자 대신 tag를 확인하면 된다. 코드가 훨씬 직관적으로 변한다 . 4-1. if문 사용 TypeScript function login(user: User) { if (user.tag === "ADMIN") { // 타입스크립트는 이제 user가 확실히 Admin임을 안다 console.log(${user.name}님 현재까지 ${user.kickCount}명 추방했습니다); } else if (user.tag === "MEMBER") { console.log(${user.name}님 현재까지 ${user.point} 모았습니다); } else { console.log(${user.name}님 현재까지 ${user.visitCount}번 오셨습니다); } } 4-2. switch문 사용 (추천) 서로소 유니온 타입은 switch 문과 함께 사용할 때 가장 빛을 발한다. 가독성이 매우 좋아진다 . TypeScript function login(user: User) { switch (user.tag) { case "ADMIN": { console.log(${user.name}님 현재까지 ${user.kickCount}명 추방했습니다); break; } case "MEMBER": { console.log(${user.name}님 현재까지 ${user.point} 모았습니다); break; } case "GUEST": { console.log(${user.name}님 현재까지 ${user.visitCount}번 오셨습니다); break; } } } --- 요약 1. 서로소 유니온 타입: tag 같은 식별 가능한 프로퍼티를 두어 타입 간의 교집합을 없앤 유니온 타입이다. 2. 장점: - 코드가 훨씬 직관적이다 (kickCount가 있는지 확인하는 것보다 tag가 ADMIN인지 확인하는 것이 명확함). - 타입 좁히기가 안전하고 정확하게 동작한다. - switch 문을 이용해 깔끔한 분기 처리가 가능하다. 3. 활용: 리액트의 reducer 액션 객체나 서버 응답 상태 처리 등 다양한 곳에서 필수적으로 사용되는 패턴이다.

TypeScriptStudyTypeGuardPatternMatching
TypeScript

[TypeScript] 타입 좁히기 (Type Narrowing)

November 27, 2025

타입스크립트에서 유니온(Union) 타입처럼 여러 타입이 합쳐진 변수를 다룰 때, 구체적인 상황에 맞게 타입을 추론해 나가는 과정을 타입 좁히기라고 한다. 이를 도와주는 코드를 타입 가드(Type Guard) 라고 부른다. 1. 왜 타입 좁히기가 필요한가? 다음과 같이 number 또는 string 타입을 매개변수로 받는 함수가 있다고 가정해 보자. TypeScript function func(value: number | string) { // value.toFixed(); // ❌ 오류 발생 // value.toUpperCase(); // ❌ 오류 발생 } 이때 함수 내부에서 바로 메서드를 사용하려고 하면 에러가 발생한다 . value가 숫자일 수도 있고 문자열일 수도 있기 때문에, 컴파일러 입장에서는 어떤 메서드를 써야 안전한지 확신할 수 없기 때문이다. 이럴 때 조건문을 이용해 "이 블록 안에서는 value가 확실히 숫자야!" 라고 보장해 주면 에러 없이 사용할 수 있다. 이를 타입 좁히기라고 한다. --- 2. typeof 타입 가드 가장 기본적인 방법은 자바스크립트의 typeof 연산자를 사용하는 것이다. 주로 원시 타입(number, string, boolean 등)을 좁힐 때 사용한다. TypeScript function func(value: number | string) { if (typeof value === "number") { // 이 안에서는 value가 number 타입으로 보장됨 console.log(value.toFixed()); } else if (typeof value === "string") { // 이 안에서는 value가 string 타입으로 보장됨 console.log(value.toUpperCase()); } } 조건문 내부에서 변수의 타입이 보장된 타입으로 좁혀지므로, 각 타입에 맞는 메서드를 안전하게 사용할 수 있다 . --- 3. instanceof 타입 가드 instanceof 연산자를 사용하면 내장 클래스(Date 등)나 직접 만든 클래스 타입을 보장할 수 있다. TypeScript function func(value: number | string | Date | null) { if (typeof value === "number") { console.log(value.toFixed()); } else if (typeof value === "string") { console.log(value.toUpperCase()); } else if (value instanceof Date) { // value가 Date 객체임이 보장됨 console.log(value.getTime()); } } 주의할 점: instanceof는 class에만 사용할 수 있다. 우리가 type 별칭이나 interface로 만든 사용자 정의 타입에는 사용할 수 없다. --- 4. in 타입 가드 직접 만든 객체 타입(Interface, Type Alias)을 좁혀야 할 때는 in 연산자를 사용한다. "객체 내부에 특정 프로퍼티가 존재하는가?" 를 확인하는 방식이다. TypeScript type Person = { name: string; age: number; }; function func(value: number | string | Date | null | Person) { if (typeof value === "number") { console.log(value.toFixed()); } else if (typeof value === "string") { console.log(value.toUpperCase()); } else if (value instanceof Date) { console.log(value.getTime()); } else if (value && "age" in value) { // value가 null이 아니고, 'age' 프로퍼티가 있다면 Person 타입으로 좁혀짐 console.log(${value.name}은 ${value.age}살 입니다); } } 위 코드에서 value && "age" in value라고 작성한 이유는 value가 null일 수도 있기 때문이다. null이 아님을 확인(value &&)한 후, age 프로퍼티가 있는지(in) 검사하여 타입을 안전하게 좁힌 것이다. --- 요약 1. 타입 좁히기: 조건문을 통해 더 구체적인 타입으로 범위를 좁히는 과정이다. 2. typeof: number, string 등 원시 타입을 좁힐 때 사용한다. 3. instanceof: Date 같은 내장 클래스나 사용자 정의 클래스를 좁힐 때 사용한다. 4. in: 커스텀 객체 타입이나 인터페이스를 좁힐 때 특정 프로퍼티 유무를 확인하여 사용한다.

TypeScriptStudyTypeGuardTypeNarrowing
TypeScript

[TypeScript] 타입 단언 (Type Assertion)

November 27, 2025

타입스크립트를 사용하다 보면, 컴파일러보다 개발자인 내가 해당 변수의 타입을 더 잘 알고 있는 경우가 있다. 이럴 때 컴파일러에게 "이 변수의 타입은 내가 확신하니까 내 말대로 처리해!"라고 명령하는 것이 바로 타입 단언(Type Assertion) 이다. 1. 타입 단언이란? (as) 변수를 선언할 때는 빈 객체로 두고 싶지만, 실제로는 특정 인터페이스나 타입의 규칙을 따르고 싶을 때가 있다. TypeScript type Person = { name: string; age: number; }; // ❌ 에러 발생: 빈 객체는 Person 타입이 아님 // let person: Person = {}; // ✅ 타입 단언 사용 let person = {} as Person; person.name = "조태민"; person.age = 25; 타입스크립트 컴파일러는 빈 객체 {}를 Person 타입으로 보지 않지만, as Person을 붙여주면 "이건 Person 타입이야"라고 단언하게 되어 에러가 사라진다 . 초과 프로퍼티 검사 회피 객체 리터럴을 직접 할당할 때 발생하는 '초과 프로퍼티 검사'도 타입 단언으로 피할 수 있다 . TypeScript type Dog = { name: string; color: string; }; let dog = { name: "깨갱이", color: "brown", breed: "리트리버", // 원래라면 에러 발생 (초과 프로퍼티) } as Dog; // ✅ 단언으로 해결 --- 2. 타입 단언의 조건 하지만 아무 타입이나 막무가내로 단언할 수 있는 것은 아니다. A as B로 단언하려면 아래 두 조건 중 하나를 반드시 만족해야 한다 . 1. A가 B의 슈퍼 타입이다. (A > B) 2. A가 B의 서브 타입이다. (A < B) 즉, 두 타입이 서로 포함 관계(교집합) 가 있어야 한다. TypeScript let num1 = 10 as never; // ✅ OK (never는 모든 타입의 서브 타입) let num2 = 10 as unknown; // ✅ OK (unknown은 모든 타입의 슈퍼 타입) // ❌ Error: number와 string은 겹치는게 없음 // let num3 = 10 as string; number와 string은 서로소 집합(공통점이 없음)이므로, 서로 단언할 수 없다 . --- 3. 다중 단언 (눈속임) 만약 억지로라도 number를 string으로 단언하고 싶다면 어떻게 해야 할까? 다중 단언을 이용하면 가능하다. TypeScript let num3 = 10 as unknown as string; - 1단계: 10 -> unknown (업캐스팅, 가능) - 2단계: unknown -> string (다운캐스팅, 가능) 중간에 만능 타입인 unknown을 끼워 넣어서 컴파일러의 눈을 속이는 방식이다. 하지만 이는 실제 값을 바꾸는 것이 아니라 컴파일러의 눈만 가리는 '눈속임'에 불과하므로, 런타임 에러가 발생할 확률이 매우 높다. 정말 어쩔 수 없는 상황이 아니라면 사용을 지양해야 한다 . --- 4. const 단언 (as const) as const는 타입 단언에서만 쓸 수 있는 특별한 문법이다. 이를 사용하면 변수를 마치 const로 선언한 것처럼 타입을 아주 좁게 추론한다. TypeScript let num4 = 10 as const; // 타입이 number가 아닌 리터럴 타입 '10'으로 단언됨 let cat = { name: "골골이", color: "yellow", } as const; // 모든 프로퍼티가 readonly(읽기 전용)가 됨 객체에 as const를 붙이면 모든 프로퍼티가 readonly가 되어, 의도치 않은 값 변경을 막는 데 유용하다 . --- 5. Non-null 단언 (!) null이나 undefined가 아님을 확신할 때 사용하는 단언이다. 값 뒤에 느낌표(!)를 붙여 사용한다 . TypeScript type Post = { title: string; author?: string; // 선택적 프로퍼티 (string | undefined) }; let post: Post = { title: "게시글1", }; // ❌ 에러: author가 undefined일 수 있음 // const len: number = post.author.length; // ✅ 해결: author는 절대 null/undefined가 아니라고 단언 (!) const len: number = post.author!.length; --- 요약 1. 타입 단언 (as): 컴파일러에게 특정 값의 타입을 강제로 지정한다. 2. 조건: 두 타입이 슈퍼-서브 관계(교집합)가 있어야 한다. 3. 다중 단언: as unknown as Type으로 강제 단언이 가능하지만 위험하다. 4. const 단언: 값을 리터럴 타입으로 만들거나 객체를 읽기 전용으로 만든다. 5. Non-null 단언 (!): 값이 null이나 undefined가 아님을 보장한다.

TypeScriptStudyTypeAssertion
TypeScript

[TypeScript] 타입 추론 (Type Inference)

November 27, 2025

타입스크립트를 사용하다 보면 모든 변수에 일일이 타입을 정의하지 않아도 코드가 잘 동작하는 것을 볼 수 있다. 이는 타입스크립트가 변수의 타입을 자동으로 파악하는 "타입 추론(Type Inference)" 기능을 제공하기 때문이다. 이번 포스팅에서는 타입스크립트가 언제, 어떻게 타입을 추론하는지, 그리고 주의해야 할 점은 무엇인지 정리해 본다. 1. 타입 추론이 가능한 상황들 일반적으로 변수를 선언하거나 초기화할 때, 타입스크립트는 할당된 값을 기준으로 타입을 추론한다. 1-1. 변수 선언 및 초기화 변수에 초기값을 할당하면 해당 값을 기준으로 타입이 추론된다. TypeScript let a = 10; // number 타입으로 추론 let b = "hello"; // string 타입으로 추론 let c = { id: 1, name: "조태민", profile: { nickname: "탬니", }, urls: ["https://taemni.dev"], }; // id, name, profile, urls 프로퍼티가 있는 객체 타입으로 추론 [cite: 207] 1-2. 구조 분해 할당 객체나 배열을 구조 분해 할당할 때도 타입 추론이 정상적으로 동작한다. TypeScript let { id, name, profile } = c; let [one, two, three] = [1, "hello", true]; 1-3. 함수의 반환값과 기본 매개변수 함수의 반환값은 return 문을 기준으로, 매개변수는 설정된 default value를 기준으로 추론된다. TypeScript // 반환값이 string으로 추론됨 function func() { return "hello"; } // message는 기본값 덕분에 string으로 추론됨 function func2(message = "hello") { return "hello"; } > 주의: 기본값이 없는 일반 매개변수는 자동으로 추론되지 않는다. 이 경우 암시적으로 any 타입이 되는데, strict 모드에서는 에러가 발생하므로 타입을 명시해줘야 한다 . > --- 2. 주의해야 할 상황들 (Gotchas) 타입 추론이 항상 단순하게 동작하는 것은 아니다. 상황에 따라 타입이 변하거나(진화), 예상보다 좁게 혹은 넓게 추론되기도 한다. 2-1. 암시적 any와 any의 진화 (Evolution of Any) 변수를 선언할 때 초기값을 생략하면, 해당 변수는 암시적으로 any 타입으로 추론된다. 재미있는 점은, 이후에 어떤 값을 할당하느냐에 따라 타입이 계속 변한다는 것이다. 이를 'any의 진화' 라고 부른다. TypeScript let d; // 암시적 any d = 10; // 이 라인부터 d는 number 타입 d.toFixed(); d = "hello"; // 이 라인부터 d는 string 타입 d.toUpperCase(); // d.toFixed(); // ❌ 오류 발생 (number 메서드 사용 불가) [cite: 241] 2-2. const 상수의 추론 (Literal Type) let으로 선언한 변수와 달리, const로 선언한 상수는 값이 변하지 않으므로 가장 좁은 타입(Literal Type) 으로 추론된다. TypeScript const num = 10; // number가 아니라 '10' (Number Literal) 타입으로 추론 const str = "hello"; // string이 아니라 '"hello"' (String Literal) 타입으로 추론 2-3. 최적 공통 타입 (Best Common Type) 배열에 다양한 타입의 요소를 넣으면, 타입스크립트는 해당 요소들을 모두 포괄할 수 있는 최적의 공통 타입(Union Type) 을 추론한다. TypeScript let arr = [1, "string"]; // (string | number)[] 타입으로 추론 [cite: 259] --- 3. 요약: 타입 추론 메커니즘 시각화 위에서 살펴본 let과 const, 그리고 any의 진화 과정을 시각적으로 정리하면 다음과 같다. ![Diagram Nov 27 2025.png](https://akodhrjerwyxttclrzkq.supabase.co/storage/v1/object/public/images/images/1764228430681-bsp7e0zx0ft.png) 핵심 요약 1. 일반 변수: 초기값을 기준으로 타입 추론 (let은 범용 타입, const는 리터럴 타입). 2. 구조 분해 & 함수: 매개변수 기본값이나 리턴값을 통해서도 추론 가능. 3. Any의 진화: 초기값 없는 변수는 할당되는 값에 따라 타입이 계속 변하는 독특한 특징을 가진다. 4. 최적 공통 타입: 여러 타입이 섞이면 Union 타입으로 추론된다.

TypeScriptStudyTypeInferenceBasics
TypeScript

[TypeScript] 대수 타입 (Union과 Intersection)

November 27, 2025

타입스크립트의 대수 타입(Algebraic Type) 은 여러 개의 타입을 합성해서 만드는 타입이다. 이 개념은 코드로만 보면 헷갈릴 수 있지만, 수학의 집합(Set) 개념인 벤 다이어그램(Venn Diagram)으로 보면 아주 명쾌하게 이해된다. 1. 합집합 - Union 타입 (|) Union 타입은 "A이거나 B이다 (OR)" 를 의미한다. 집합으로 표현하면 두 원형의 모든 영역(합집합) 이 유효한 타입의 범위가 된다. 1-1. 시각적 개념도 String | Number 타입을 그림으로 상상해 보자. Plaintext [ String | Number ] ╭────────╮ ╭────────╮ │ │ │ │ │ String │ U │ Number │ │ (OK) │ │ (OK) │ │ │ │ │ ╰────────╯ ╰────────╯ 👉 둘 중 하나만 만족하면 통과! 색칠된 모든 범위가 집합에 포함되므로, string도 되고 number도 된다. 1-2. 객체 타입에서의 Union (함정 주의!) 객체 타입에서 Union을 쓸 때 많은 사람이 헷갈려 한다. 다음 예시를 보자. TypeScript type Dog = { name: string; color: string; }; type Person = { name: string; language: string; }; type Union1 = Dog | Person; 이 타입을 벤 다이어그램으로 그리면 다음과 같다. Plaintext [ Union1 타입의 허용 범위 ] ( Dog ) ( Person ) ╭─────────────╮ ╭─────────────╮ │ name │ │ name │ │ color │ │ language │ │ │ │ │ │ (1) │(3)│ (2) │ │ 🐶 Dog만 │ 교 │ 👤 Person만 │ │ 만족해도 OK │ 집 │ 만족해도 OK │ │ │ 합 │ │ ╰─────────────╯ ╰─────────────╯ 이 그림에 따르면 다음 3가지 경우가 모두 허용된다 . 1. (1) 영역: Dog 타입만 만족하는 객체 (name, color) ✅ 2. (2) 영역: Person 타입만 만족하는 객체 (name, language) ✅ 3. (3) 영역: 둘 다 만족하는 객체 (name, color, language) ✅ 하지만, 어느 원 안에도 들어가지 못하는 경우는 에러가 난다. TypeScript // ❌ (4) 영역 밖: Dog도 아니고 Person도 아님 let fail: Union1 = { name: "누구세요" // color나 language가 없어서 어느 집합에도 못 들어감 }; 이처럼 Union 타입은 "적어도 하나의 타입 조건은 완벽히 갖춰야 한다" 는 것을 그림으로 기억하면 쉽다 . --- 2. 교집합 - Intersection 타입 (&) Intersection 타입은 "A이면서 동시에 B이다 (AND)" 를 의미한다. 집합으로 표현하면 두 원형이 겹치는 부분(교집합) 만이 유효한 타입의 범위가 된다. 2-1. 시각적 개념도 Dog & Person 타입을 그림으로 보면 가운데 겹친 부분만 색칠된다. Plaintext [ Dog & Person (Intersection) ] ( Dog ) ( Person ) ╭─────────────╮ ╭─────────────╮ │ │ 빗 │ │ │ (Invalid) │ 금 │ (Invalid) │ │ │ 칠 │ │ │ Dog만 있어도 │ 한 │ Person만 │ │ 탈락! │ 곳 │ 있어도 탈락! │ │ │ 만 │ │ ╰─────────────╯ OK ╰─────────────╯ 2-2. 객체 타입 합성 (프로퍼티 합치기) Intersection 타입은 두 집합의 모든 속성을 다 가지고 있어야 교집합 영역(가운데)에 들어갈 수 있다. 즉, 프로퍼티가 합쳐지는 효과가 난다. TypeScript type Dog = { name: string; color: string; }; type Person = { name: string; language: string; }; type Inter = Dog & Person; 이 타입이 되려면 Dog의 조건과 Person의 조건을 동시에 만족해야 한다. TypeScript let hybrid: Inter = { name: "개사람", color: "brown", // Dog의 조건 language: "woof", // Person의 조건 }; 결국 Intersection 타입은 객체에서 "모든 프로퍼티를 다 때려 넣은 슈퍼 객체" 를 만들 때 사용된다고 이해하면 된다. 2-3. 기본 타입의 교집합 (never) 만약 서로 겹칠 수 없는 타입끼리 교집합을 만들면 어떻게 될까? TypeScript let nothing: number & string; Plaintext [ number ] [ string ] ╭────────╮ ╭────────╮ │ 1 │ │ "a" │ │ 2 │ X │ "b" │ ╰────────╯ ╰────────╯ 서로 겹치는 부분이 없음 (공집합) 숫자이면서 동시에 문자인 값은 존재하지 않는다. 교집합이 없으므로 이 타입은 never (공집합) 가 된다. --- 요약 1. Union (|): 두 원을 모두 색칠한 그림. 하나만 만족해도 통과. 2. Intersection (&): 두 원의 가운데만 색칠한 그림. 모든 조건을 다 만족해야 통과 (프로퍼티 합체).

TypeScriptStudyTypeSystemUnionIntersection
TypeScript

[TypeScript] 객체 타입의 호환성과 초과 프로퍼티 검사

November 27, 2025

지난 포스팅에서 "타입은 집합이다" 라는 개념을 통해 업캐스팅(자식→부모)은 가능하고, 다운캐스팅(부모→자식)은 불가능하다는 것을 배웠다. 이 원칙은 객체(Object) 타입에서도 똑같이 적용된다. 하지만 객체는 프로퍼티의 개수에 따라 부모-자식 관계가 조금 헷갈릴 수 있다. 이를 명확히 짚어보자. 1. 객체 타입의 호환성 간단한 예제로 시작해 보자. ts type Animal = { name: string; color: string; }; type Dog = { name: string; color: string; breed: string; // 추가된 프로퍼티 }; 여기서 Animal과 Dog 중 어느 것이 슈퍼 타입(부모) 이고, 어느 것이 서브 타입(자식) 일까? 정답은 Animal이 슈퍼 타입이다. 얼핏 보면 Dog가 프로퍼티가 더 많으니 슈퍼 타입 같지만, 집합의 관점에서 보면 반대다. - Animal: name과 color만 있으면 다 포함된다. (더 넓은 범위) - Dog: name, color에 breed까지 있어야 한다. (더 좁고 구체적인 범위) 따라서 Dog는 Animal의 부분 집합(서브 타입)이 된다. 업캐스팅 적용 ts let animal: Animal = { name: "기린", color: "yellow", }; let dog: Dog = { name: "돌돌이", color: "brown", breed: "진도", }; animal = dog; // ✅ OK (업캐스팅: Dog -> Animal) // dog = animal; // ❌ NO (다운캐스팅: Animal -> Dog) dog 변수는 Animal이 필요로 하는 name과 color를 모두 가지고 있다. 따라서 Animal 타입으로 취급해도 아무런 문제가 없다. 이것이 구조적 타이핑(Structural Typing) 의 핵심이다. --- 2. 또 다른 예시 (Book) 이해를 돕기 위해 예제 하나를 더 보자. ts type Book = { name: string; price: number; }; type ProgrammingBook = { name: string; price: number; skill: string; }; 여기서도 Book이 슈퍼 타입, ProgrammingBook이 서브 타입이다. ts let book: Book; let programmingBook: ProgrammingBook = { name: "한 입 크기로 잘라먹는 리액트", price: 33000, skill: "reactjs", }; book = programmingBook; // ✅ OK // programmingBook = book; // ❌ NO --- 3. 예외: 초과 프로퍼티 검사 (Excess Property Check) 그런데 타입스크립트에는 업캐스팅임에도 불구하고 에러를 발생시키는 특수한 규칙이 있다. 바로 초과 프로퍼티 검사다. 문제 상황 위의 Book 예제를 그대로 사용하여, 변수에 객체 리터럴을 직접 할당해 보자. ts let book2: Book = { name: "한 입 크기로 잘라먹는 리액트", price: 33000, skill: "reactjs", // ❌ 에러 발생! (Object literal may only specify known properties...) }; "어? skill이 있어도 name과 price가 있으니까 Book 타입에 들어갈 수 있는 거(업캐스팅) 아닌가?" 논리적으로는 맞다. 하지만 타입스크립트는 변수를 초기화할 때 '객체 리터럴'을 직접 사용하면, 타입에 정의되지 않은 초과된 프로퍼티가 있는지 엄격하게 검사한다. 이를 방지하기 위한 안전장치다. 해결 방법: 변수에 담아서 할당하기 이 검사는 객체 리터럴을 직접 대입할 때만 발동한다. 따라서 값을 다른 변수에 미리 담아둔 뒤 할당하면 검사를 피할 수 있다. ts // 미리 만들어둔 변수 (ProgrammingBook 타입으로 추론되거나 명시됨) let programmingBook = { name: "한 입 크기로 잘라먹는 리액트", price: 33000, skill: "reactjs", }; // 변수를 할당하면 초과 프로퍼티 검사가 발동하지 않음 let book3: Book = programmingBook; // ✅ OK 함수 인수의 경우 함수에 인수를 전달할 때도 동일하게 적용된다. ts function func(book: Book) {} // 1. 객체 리터럴 직접 전달 -> 검사 발동 (에러) func({ name: "리액트", price: 33000, // skill: "reactjs", // ❌ 에러 발생 }); // 2. 변수에 담아서 전달 -> 검사 회피 (성공) func(programmingBook); // ✅ OK --- 요약 1. 객체 타입 호환성: 프로퍼티가 더 적은 쪽이 슈퍼 타입(부모), 더 많은 쪽이 서브 타입(자식) 이다. 2. 구조적 타이핑: 서브 타입 객체를 슈퍼 타입 변수에 할당하는 것(업캐스팅)은 언제나 가능하다. 3. 초과 프로퍼티 검사: 단, 객체 리터럴을 직접 할당할 때는 타입에 없는 속성이 있으면 에러가 난다. 이를 피하려면 변수에 담아서 할당하면 된다.

TypeScriptFrontEndStudyTypeSystemInterface
TypeScript

[TypeScript] 타입 계층도로 살펴보는 기본 타입 (unknown, never, void, any)

November 27, 2025

지난 포스팅에서 "타입은 집합이다" 라는 사실을 배웠다. 이번에는 이 개념을 확장해서 타입스크립트의 전체 족보, 즉 타입 계층도를 뜯어보자. 계층도의 위치에 따라 타입 간의 호환성(업캐스팅, 다운캐스팅)이 결정된다. 1. 타입 계층도 (Type Hierarchy) 먼저 전체적인 그림을 보자. 가장 위에는 unknown이 있고, 가장 아래에는 never가 있다. 그리고 그 사이를 any가 가로지르고 있다. ![계층타입.png](https://akodhrjerwyxttclrzkq.supabase.co/storage/v1/object/public/images/images/1764215478342-qpmqxcxn4uj.png) --- 2. unknown 타입 (전체 집합) unknown 타입은 타입 계층도의 최상단에 위치한다. 집합으로 따지면 모든 타입을 포함하는 전체 집합(Universal Set) 이다. 특징 1. 모든 타입을 받을 수 있다 (업캐스팅): unknown은 모든 타입의 부모(슈퍼 타입)이므로, 어떤 값이든 할당할 수 있다. 2. 아무데도 들어갈 수 없다 (다운캐스팅 불가): 부모가 자식 집합에 들어갈 수 없듯이, unknown 타입의 값은 any를 제외한 그 어떤 타입의 변수에도 할당할 수 없다. ts let a: unknown = 1; // ✅ 가능 (number -> unknown) let b: unknown = "hello"; // ✅ 가능 (string -> unknown) let unknownVal: unknown; // let num: number = unknownVal; // ❌ 불가능 (unknown -> number) --- 3. never 타입 (공집합) never 타입은 타입 계층도의 가장 아래에 위치한다. 집합으로 따지면 아무것도 포함하지 않는 공집합(Empty Set) 이다. 특징 1. 값이 없다: 공집합이므로 never 타입에 해당하는 값은 존재하지 않는다. 2. 모든 타입에 들어갈 수 있다 (업캐스팅): 공집합은 모든 집합의 부분 집합이다. 따라서 never는 모든 타입의 자식(서브 타입)이며, 모든 타입으로 할당 가능하다. 3. 아무것도 받을 수 없다 (다운캐스팅 불가): 그 어떤 타입도 never 타입으로 다운캐스팅할 수 없다. 사용 예시 주로 정상적으로 종료되지 않는(반환값이 없는) 함수에 사용된다. ts function errorFunc(): never { throw new Error(); } let num: number = errorFunc(); // ✅ 가능 (never -> number) // let n: never = 10; // ❌ 불가능 (number -> never) --- 4. void 타입 void는 아무것도 반환하지 않는 함수의 반환값으로 주로 쓰인다. 계층도상에서 void는 undefined 타입의 슈퍼 타입(부모)에 해당한다. 특징 1. undefined를 품는다: undefined는 void의 서브 타입이므로, void 타입 변수에 undefined를 할당할 수 있다. 2. 엄격한 할당: void에는 undefined와 never를 제외하고는 다른 값을 할당할 수 없다. ts function noReturn(): void { return undefined; // ✅ 가능 } let voidVar: void; voidVar = undefined; // ✅ 가능 --- 5. any 타입 (치트키) any 타입은 사실상 타입 계층도를 무시하는 치트키다. 모든 타입의 슈퍼 타입이 될 수도 있고, 서브 타입이 될 수도 있는 예외적인 존재다. 특징 1. 모든 타입 -> any (업캐스팅): 어떤 값이든 any 변수에 넣을 수 있다. 2. any -> 모든 타입 (다운캐스팅): any 변수는 어떤 타입의 변수에도 넣을 수 있다. ts let anyValue: any; // 모든 값을 다 받음 (업캐스팅) anyValue = 10; anyValue = "hello"; // 모든 곳에 다 들어감 (다운캐스팅) let num: number = anyValue; let str: string = anyValue; any는 타입 시스템의 규칙을 파괴하므로, 사용에 각별한 주의가 필요하다. --- 요약 - unknown: 전체 집합 (최상위). 모든 값을 받지만, 어디에도 못 간다. - never: 공집합 (최하위). 값을 못 받지만, 어디든 갈 수 있다. - void: undefined의 부모. - any: 계층 파괴자. 규칙을 무시하고 어디든 오고 간다.

TypeScriptStudyTypeHierarchy기본타입
TypeScript

[TypeScript] 타입은 집합이다

November 26, 2025

타입스크립트를 공부하다 보면 "이 타입은 저 변수에 넣을 수 있는데, 왜 반대는 안 되지?"라는 의문이 생길 때가 있다. 이를 이해하기 위해서는 "타입은 집합(Set)이다" 라는 개념을 잡고 가야 한다. 1. 타입은 집합이다 수학 시간에 배운 집합을 떠올려보자. 집합은 동일한 속성을 갖는 여러 개의 요소를 하나의 그룹으로 묶은 단위를 말한다. 타입스크립트의 타입도 이와 똑같다. Number 타입 vs Number 리터럴 타입 예를 들어 number 타입은 우리가 아는 모든 숫자(-20, 0, 3, Infinity, NaN 등)를 포함하는 거대한 집합이다. 반면, 우리가 이전에 배운 Number Literal 타입(예: 20)은 어떨까? 이 타입은 오직 20이라는 딱 하나의 값만 포함하는 아주 작은 집합이다. 20은 숫자이므로, Number Literal Type은 결국 Number Type이라는 거대한 집합에 포함되는 부분 집합이 된다. --- 2. 슈퍼 타입(부모)과 서브 타입(자식) 타입스크립트의 모든 타입은 서로 포함하고 포함되는 관계를 갖는다. 이 관계를 부모와 자식 관계로 정의할 수 있다. - 슈퍼 타입 (Super Type, 부모): 다른 타입을 포함하는 더 큰 집합 (예: number). - 서브 타입 (Sub Type, 자식): 다른 타입에 포함되는 더 작은 집합 (예: number literal). 이 계층 구조를 전체적으로 보면 아래와 같다. unknown이 최상위 슈퍼 타입이고, never가 최하위 서브 타입이 된다. - 전체 계층도![타입트리.png](https://akodhrjerwyxttclrzkq.supabase.co/storage/v1/object/public/images/images/1764181629157-ioczwt12x7j.png) --- 3. 타입 호환성 (Type Compatibility) 이제 타입 호환성이라는 개념을 이해할 수 있다. 타입 호환성이란 "A 타입의 값을 B 타입으로 취급해도 괜찮은지"를 판단하는 것이다. 3-1. 업 캐스팅 (Upcasting) : OK ✅ 서브 타입(자식)의 값을 슈퍼 타입(부모)의 값으로 취급하는 것은 가능하다. 이를 업 캐스팅이라고 부르며, 모든 상황에서 안전하게 가능하다. - 예: "정사각형(자식)은 직사각형(부모)이다" (참) - 예: "숫자 10(리터럴)은 숫자(number)이다" (참) ts let num1: number = 10; let num2: 10 = 10; // num2(작은 집합)를 num1(큰 집합)에 넣는 것 num1 = num2; // ✅ 가능 (업 캐스팅) 3-2. 다운 캐스팅 (Downcasting) : NO ❌ 반대로 슈퍼 타입(부모)의 값을 서브 타입(자식)의 값으로 취급하는 것은 불가능하다. 이를 다운 캐스팅이라고 부르며, 대부분의 상황에서 허용되지 않는다. - 예: "직사각형(부모)은 정사각형(자식)이다" (거짓) - 예: "모든 숫자(number)는 10(리터럴)이다" (거짓) ts let num1: number = 10; let num2: 10 = 10; // num1(큰 집합)을 num2(작은 집합)에 넣는 것 // num2 = num1; // ❌ 불가능 (다운 캐스팅 에러) 왜 안 될까?num1은 number 타입이므로 10 외에도 999, -5 같은 다양한 숫자를 가질 수 있다. 그런데 num2는 오직 10만 담을 수 있는 그릇이다. 더 큰 범위의 값을 더 작은 그릇에 억지로 구겨 넣으려고 하면 넘치거나 문제가 생길 수밖에 없다. --- 요약 1. 타입은 집합이다: 타입은 값들의 모음(Set)으로 볼 수 있다. 2. 계층 관계: 더 큰 집합은 슈퍼 타입(부모), 포함되는 작은 집합은 서브 타입(자식)이다. 3. 타입 호환성: - 작은 것을 큰 것에 넣는 것(업 캐스팅)은 가능하다. - 큰 것을 작은 것에 넣는 것(다운 캐스팅)은 불가능하다.

TypeScriptStudyTypeTheory타입호환성
TypeScript

[TypeScript] void 타입과 never 타입

November 26, 2025

이번에는 함수의 반환값과 관련된 특수한 타입인 void 와 never 에 대해 알아본다. 얼핏 보면 비슷해 보일 수 있지만, 쓰임새와 의미가 명확히 다르다. 1. void 타입 void는 '아무런 값도 없음' 을 의미하는 타입이다. 1-1. 함수의 반환값으로 사용 주로 값을 반환하지 않는 함수의 반환 타입으로 사용된다. 자바스크립트에서는 함수에서 아무것도 반환하지 않으면 암묵적으로 undefined를 반환하지만, 타입스크립트에서는 이를 void로 표현한다. ts function func2(): void { console.log("Hello"); } 1-2. 변수의 타입으로 사용 변수에도 void 타입을 지정할 수 있지만, 실용성은 거의 없다. void 타입 변수에는 오직 undefined만 할당할 수 있기 때문이다. ts let a: void; a = undefined; // 가능 // a = 1; // ❌ 불가능 > 참고: strictNullChecks 옵션 만약 tsconfig.json에서 strictNullChecks 옵션을 false로 설정하면, void 타입 변수에 null 값을 담을 수도 있다. (하지만 엄격한 옵션을 켜두는 것이 권장된다.) > --- 2. never 타입 never는 '불가능' 을 의미하는 타입이다. 즉, 절대 발생할 수 없는 값이나 상태를 나타낼 때 사용한다. 2-1. 함수가 종료되지 않을 때 함수가 무한 루프를 돌아 영원히 종료되지 않는다면, 이 함수는 어떤 값도 반환할 수 없다(반환 자체가 불가능). 이때 never를 사용한다. ts function func3(): never { while (true) { // 무한 루프 } } 2-2. 의도적으로 오류를 발생시킬 때 함수 실행 도중 에러를 던져서(throw) 강제로 실행을 중단시키는 경우에도, 정상적인 반환이 불가능하므로 never 타입을 사용한다. ts function func4(): never { throw new Error(); } 2-3. 변수에 사용 시 (가장 엄격한 타입) 변수의 타입을 never로 정의하면, 그 변수에는 어떤 값도 담을 수 없다. 심지어 모든 타입의 치트키인 any 타입의 값조차도 never 타입 변수에는 할당할 수 없다. ts let anyVar: any; let a: never; // a = 1; // ❌ 오류 // a = undefined; // ❌ 오류 // a = null; // ❌ 오류 // a = anyVar; // ❌ 오류 (any도 안 됨) 이러한 특징 때문에 never는 보통 코드의 흐름상 절대 도달할 수 없는 코드임을 명시하거나, 조건문에서 모든 케이스를 다루었는지 검사(Exhaustiveness Checking)할 때 유용하게 쓰인다. --- 요약 1. void: "비어있음". 아무 값도 반환하지 않는 함수에 사용한다. (undefined만 호환) 2. never: "불가능". 영원히 끝나지 않거나 에러를 던지는 함수에 사용한다. (그 어떤 값도 호환되지 않음)

TypeScriptStudy기초문법TypeSafety
TypeScript

[TypeScript] Any 타입과 Unknown 타입

November 26, 2025

타입스크립트를 쓰다 보면 타입을 미리 알 수 없거나, 유연하게 처리해야 할 상황이 생긴다. 이때 사용할 수 있는 것이 any와 unknown이다. 두 타입 모두 모든 값을 허용한다는 공통점이 있지만, 안정성 면에서 큰 차이가 있다. 1. Any 타입 (치트키) any 타입은 타입스크립트의 모든 타입 검사를 무력화시키는 일종의 치트키(Cheat Key) 같은 타입이다. 1-1. 무제한의 자유 일반적으로 타입스크립트는 초기화된 값을 기준으로 타입을 추론하거나, 명시된 타입 외의 값을 넣으면 에러를 뱉는다. 하지만 any를 쓰면 어떤 타입의 값이든 할당할 수 있다. ts let anyVar: any = 10; anyVar = "hello"; // 문제없음 anyVar = true; // 문제없음 anyVar = {}; // 문제없음 심지어 해당 값에 어떤 메서드를 쓰든, 어떤 연산을 하든 터치하지 않는다. ts anyVar.toUpperCase(); // 런타임에 에러가 날 수도 있지만, 컴파일은 통과됨 anyVar.toFixed(); anyVar.a; 1-2. 모든 타입에 할당 가능 any 타입의 변수는 다른 정적인 타입(number 등)의 변수에도 자유롭게 할당할 수 있다. ts let anyVar: any = 10; let num: number = 20; num = anyVar; // ❌ 문제 발생! num은 number 타입인데 any가 들어와 버림 1-3. Any의 위험성 any는 편해 보이지만 매우 위험하다. 컴파일 시점에 에러를 잡지 못하고 런타임(실행 중)에 에러가 터질 수 있기 때문이다. any를 남발하면 타입스크립트를 쓰는 이유가 사라진다. 따라서 정말 어쩔 수 없는 경우를 제외하고는 사용하지 않는 것을 강력히 권장한다. --- 2. Unknown 타입 (안전한 Any) unknown 타입은 any와 비슷하게 모든 타입의 값을 저장할 수 있지만, 훨씬 더 안전하다. 2-1. 모든 값을 받을 수 있다 any처럼 변수에 어떤 타입의 값이든 넣을 수 있다. ts let unknownVar: unknown; unknownVar = ""; unknownVar = 1; unknownVar = () => {}; 2-2. 하지만 사용은 엄격하다 값을 넣을 때는 자유롭지만, 그 값을 사용하거나 다른 변수에 넣을 때는 엄격한 제한이 걸린다. 1) 다른 타입의 변수에 할당 불가unknown 타입의 값은 오직 any나 unknown 타입의 변수에만 할당할 수 있다. ts let num: number = 10; let unknownVar: unknown = 20; // num = unknownVar; // ❌ 에러 발생 (Type 'unknown' is not assignable to type 'number') 2) 연산 및 메서드 사용 불가 값의 타입이 확실하지 않기 때문에 연산이나 메서드 호출도 막혀 있다. ts let unknownVar: unknown = 30; // unknownVar 2; // ❌ 에러 발생 // unknownVar.toUpperCase(); // ❌ 에러 발생 정리하자면 unknown은 "값을 저장하는 행위"만 가능하고, 그 값을 사용하려면 반드시 타입을 확인하는 과정을 거쳐야 한다. --- 3. Unknown 타입 올바르게 사용하기 (타입 좁히기) unknown 타입의 값을 사용하려면, 해당 값이 어떤 타입인지 확실히 검사해줘야 한다. 이를 타입 좁히기(Type Narrowing) 라고 한다. ts let unknownVar: unknown = 10; if (typeof unknownVar === "number") { // 이 블록 안에서는 unknownVar가 number 타입으로 취급된다. console.log(unknownVar 2); // ✅ 정상 작동 } 조건문을 통해 타입이 확인되면, 타입스크립트는 해당 블록 내부에서 변수의 타입을 자동으로 추론해 준다. --- 요약 1. Any 타입: 모든 검사를 무시하는 치트키. 타입 안정성을 해치므로 사용을 지양해야 한다. 2. Unknown 타입: 모든 값을 저장할 수 있지만, 사용하려면 타입을 검사해야 한다. any보다 안전하다. 3. 결론: 변수의 타입을 미리 알 수 없다면 any 대신 unknown을 사용하는 것이 훨씬 안전하다.

TypeScriptStudy기초문법TypeSafety
TypeScript

[TypeScript] 열거형(Enum) 타입

November 26, 2025

열거형(Enum) 타입은 이름 그대로 여러 개의 값을 나열하는 용도로 사용한다. 자바스크립트에는 존재하지 않고 오직 타입스크립트에서만 사용할 수 있는 특별한 타입이다. 1. 숫자형 Enum (Numeric Enum) 가장 기본적인 형태의 Enum이다. 유저의 권한(Role)처럼 관련된 상수들의 집합을 정의할 때 유용하다. 1-1. 기본 자동 할당 별도로 값을 지정하지 않으면 0부터 1씩 증가하는 값이 자동으로 할당된다. ts enum Role { ADMIN, // 0 할당 (자동) USER, // 1 할당 (자동) GUEST, // 2 할당 (자동) } const user1 = { name: "탬니", role: Role.ADMIN, // 0 }; const user2 = { name: "홍길동", role: Role.USER, // 1 }; const user3 = { name: "아무개", role: Role.GUEST, // 2 }; 이렇게 Enum을 사용하면 role: 0처럼 의미를 알 수 없는 숫자 대신 Role.ADMIN을 사용하여 코드의 가독성을 높일 수 있다. 1-2. 값 직접 할당 및 자동 증가 물론 0이 아닌 다른 숫자를 할당할 수도 있다. 시작하는 멤버에 값을 주면, 그 뒤의 멤버들은 자동으로 1씩 증가한다. ts enum Role { ADMIN = 10, // 10 할당 USER, // 11 할당 (자동) GUEST, // 12 할당 (자동) } 물론 모든 멤버에 각각 다른 값을 직접 지정하는 것도 가능하다. ts enum Role { ADMIN = 0, USER = 1, GUEST = 2, } --- 2. 문자열 Enum (String Enum) Enum의 멤버에는 숫자뿐만 아니라 문자열 값도 할당할 수 있다. 이를 문자열 Enum이라고 부른다. ts enum Language { korean = "ko", english = "en", } const user1 = { name: "이정환", role: Role.ADMIN, language: Language.korean, // "ko" }; 왜 문자열 Enum을 쓸까? 문자열을 직접 타이핑하다 보면 오타가 발생할 수 있다. 예를 들어 "ko"라고 적어야 하는데 "kos", "KO-kr" 등으로 실수할 수 있는데, Enum을 사용하면 Language.korean처럼 자동 완성을 이용하므로 실수를 방지하고 코드를 안전하게 작성할 수 있다. --- 3. Enum은 컴파일되어도 사라지지 않는다 보통 타입스크립트의 타입(Type Alias, Interface 등)은 컴파일 시점에 모두 제거된다. 하지만 Enum은 컴파일 결과에 객체로 남는다. 컴파일 전 (TypeScript) ts enum Role { ADMIN, USER, GUEST, } 컴파일 후 (JavaScript) js var Role; (function (Role) { Role[Role["ADMIN"] = 0] = "ADMIN"; Role[Role["USER"] = 1] = "USER"; Role[Role["GUEST"] = 2] = "GUEST"; })(Role || (Role = {})); Enum은 내부적으로 자바스크립트 객체로 변환된다. 따라서 런타임(실행 중)에도 Role.ADMIN과 같이 값으로 접근하여 사용할 수 있다. --- 요약 1. Enum: 연관된 상수들을 모아놓은 타입으로 자바스크립트에는 없는 기능이다. 2. 숫자형 Enum: 0부터 숫자가 자동 할당되며, 시작 값을 지정하면 1씩 자동 증가한다. 3. 문자열 Enum: 의미 있는 문자열 값을 지정해 오타 방지 및 가독성을 높일 수 있다. 4. 컴파일: 다른 타입들과 달리 컴파일 후에도 사라지지 않고 자바스크립트 객체로 남는다.

TypeScriptStudy기초문법Enum
TypeScript

[TypeScript] 타입 별칭(Type Alias)과 인덱스 시그니처(Index Signature)

November 26, 2025

매번 복잡한 객체 타입을 정의할 때마다 긴 코드를 반복해서 작성하는 것은 비효율적이다. 이번에는 타입을 변수처럼 정의해 재사용성을 높여주는 타입 별칭과, 객체의 프로퍼티를 유연하게 관리하게 해주는 인덱스 시그니처에 대해 알아본다. 1. 타입 별칭 (Type Alias) 타입 별칭을 이용하면 변수를 선언하듯 타입을 별도로 정의하고 이름을 붙여줄 수 있다. 1-1. 기본 정의 및 사용 type 키워드를 사용하여 정의한다. ts // 타입 별칭 정의 type User = { id: number; name: string; nickname: string; birth: string; bio: string; location: string; }; 이렇게 만든 User 타입은 변수의 타입을 정의할 때 간편하게 사용할 수 있다. ts let user: User = { id: 1, name: "탬니", nickname: "taemni", birth: "2xxx.xx.xx", bio: "반갑습니다~", location: "xx시", }; let user2: User = { id: 2, name: "홍길동", nickname: "길동이", birth: "1xxx.xx.xx", bio: "안녕하세요", location: "xx시", }; 매번 객체 리터럴로 타입을 길게 명시하지 않아도 되므로 코드의 가독성이 좋아지고 중복이 줄어든다. 1-2. 스코프 (Scope) 규칙 타입 별칭도 변수처럼 스코프의 규칙을 따른다. 1. 동일한 스코프 내 중복 선언 불가: 같은 이름(User)으로 두 번 정의하면 에러가 발생한다. 2. 다른 스코프 내 중복 선언 가능: 함수 내부 등 다른 스코프라면 같은 이름을 사용할 수 있다. ts type User = { id: number; name: string; // ... }; // type User = {}; // ❌ 에러 발생 (같은 스코프) function test() { type User = string; // ✅ 가능 (함수 내부 스코프) } 위 코드에서 test 함수 내부의 User는 string 타입이고, 함수 외부의 User는 객체 타입이 된다. 1-3. 컴파일 시점 제거 타입 별칭은 타입스크립트의 문법이므로 컴파일되어 자바스크립트로 변환되면 흔적도 없이 사라진다. 런타임 코드에 영향을 주지 않는다. --- 2. 인덱스 시그니처 (Index Signature) 인덱스 시그니처는 객체의 키(Key)와 값(Value)의 규칙만 정의하여 타입을 유연하게 만드는 문법이다. 2-1. 사용하는 이유 예를 들어 국가 코드를 저장하는 객체가 있다고 가정해보자. 국가가 3개라면 일일이 정의할 수 있지만, 만약 100개가 넘는다면? ts // 100개를 다 적으려면 너무 힘들다... type CountryCodes = { Korea: string; UnitedState: string; UnitedKingdom: string; // ... (나머지 97개) }; 이럴 때 인덱스 시그니처를 사용하면 간단하게 정의할 수 있다. 2-2. 문법 및 사용 [key: 타입]: 타입 형태로 작성한다. ts type CountryCodes = { [key: string]: string; }; let countryCodes: CountryCodes = { Korea: "ko", UnitedState: "us", UnitedKingdom: "uk", Brazil: "bz", // 자유롭게 추가 가능 }; 위 코드는 "key가 string 타입이고, value도 string 타입인 모든 프로퍼티를 허용한다" 는 의미이다. 따라서 Korea, Brazil 등 어떤 문자열 키가 들어와도 값이 문자열이라면 모두 허용된다. 2-3. 특정 프로퍼티와 혼용 및 주의사항 인덱스 시그니처를 사용하면서 동시에 필수 프로퍼티를 명시할 수도 있다. ts type CountryNumberCodes = { [key: string]: number; Korea: number; // Korea는 반드시 있어야 함 }; ⚠️ 주의할 점: 인덱스 시그니처와 개별 프로퍼티를 섞어 쓸 때, 개별 프로퍼티의 타입은 반드시 인덱스 시그니처 타입과 일치하거나 호환되어야 한다. ts type CountryNumberCodes = { [key: string]: number; // 모든 값은 number여야 함 // Korea: string; // ❌ 에러 발생! (string은 number에 할당 불가) }; 인덱스 시그니처가 number를 반환한다고 선언했으므로, Korea 속성 또한 number여야 규칙이 깨지지 않는다. --- 요약 1. 타입 별칭: type 키워드로 타입을 변수처럼 정의해 재사용한다. 컴파일 시 사라진다. 2. 인덱스 시그니처: [key: T]: U 문법으로 객체의 속성을 유연하게 정의한다. 3. 주의: 인덱스 시그니처와 함께 쓰는 구체적인 속성은 인덱스 시그니처의 타입과 호환되어야 한다.

TypeScriptStudy기초문법Type AliasIndex Signature
TypeScript

[TypeScript] 객체(Object) 타입과 구조적 타이핑

November 26, 2025

이번에는 타입스크립트에서 객체(Object) 의 타입을 정의하는 방법과 구조적 타입 시스템, 그리고 선택적/읽기 전용 프로퍼티 같은 특수 문법에 대해 알아본다. 1. object 타입 타입스크립트에서 객체의 타입을 정의하는 가장 간단한 방법은 object 키워드를 사용하는 것이다. ts let user: object = { id: 1, name: "탬니", }; 하지만 이렇게 object로 타입을 정의하면 한 가지 치명적인 문제가 발생한다. ts // user.id; // ❌ 에러 발생 (Property 'id' does not exist on type 'object') 왜 에러가 날까?object 타입은 해당 변수가 '객체이다' 라는 사실만 알려줄 뿐, 그 객체 안에 어떤 프로퍼티가 있는지에 대한 정보는 전혀 없기 때문이다. 따라서 실제 프로퍼티에 접근하려고 하면 에러가 발생한다. 우리가 원하는 것은 객체의 구체적인 구조까지 타입으로 정의하는 것이다. 이때 사용하는 것이 바로 객체 리터럴 타입이다. 2. 객체 리터럴 타입 객체 리터럴 타입은 중괄호 {}를 열고, 객체가 가져야 할 프로퍼티의 이름과 타입을 직접 나열하여 정의한다. ts let user: { id: number; name: string; } = { id: 1, name: "탬니", }; console.log(user.id); // 1 (정상 작동) 이제 타입스크립트는 user 객체 안에 id가 숫자형이고, name이 문자열이라는 것을 정확히 인지한다. 점 표기법을 사용할 때 자동 완성이 지원되고, 타입 검사도 수행된다. 구조적 타입 시스템 (Structural Type System) 여기서 알 수 있는 중요한 사실은, 타입스크립트가 프로퍼티를 기준으로 객체의 타입을 정의한다는 점이다. - 명목적 타입 시스템 (Nominal Type System): C, Java처럼 클래스나 인터페이스의 '이름'을 기준으로 타입을 따지는 방식. - 구조적 타입 시스템 (Structural Type System): 타입스크립트처럼 이름이 무엇이든 "어떤 구조(프로퍼티)를 가지고 있느냐" 를 기준으로 타입을 따지는 방식. 마치 "이름(name)과 색깔(color)이 있으면 그건 강아지 객체야"라고 판단하는 것과 같다. 이를 프로퍼티 베이스드 타입 시스템(Property-based Type System) 이라고도 부른다. ts let dog: { name: string; color: string; } = { name: "뽀삐", color: "white", }; 3. 특수한 프로퍼티 정의하기 객체 타입을 정의할 때 상황에 따라 프로퍼티를 선택적으로 만들거나, 수정할 수 없게 만들 수 있다. 3-1. 선택적 프로퍼티 (Optional Property) 객체를 다루다 보면 특정 프로퍼티가 있을 수도 있고 없을 수도 있는 경우가 있다. 예를 들어 이름은 있지만 id는 아직 발급되지 않은 유저가 있을 수 있다. 이때 프로퍼티 이름 뒤에 물음표(?) 를 붙여주면 된다. ts let user: { id?: number; // 있어도 되고 없어도 됨 name: string; } = { id: 1, name: "탬니", }; // id가 없는 객체를 할당해도 에러가 나지 않음 user = { name: "홍길동", }; 주의할 점: 선택적 프로퍼티라 하더라도, 만약 값이 존재한다면 반드시 정의된 타입(number)이어야 한다. 엉뚱한 타입(string 등)을 넣으면 에러가 발생한다. 3-2. 읽기 전용 프로퍼티 (Readonly Property) API 키나 고유 ID처럼, 객체 생성 시 한 번 값이 정해지면 절대 바뀌면 안 되는 값들이 있다. 이때는 프로퍼티 이름 앞에 readonly 키워드를 붙인다. ts let config: { readonly apiKey: string; // 읽기 전용 } = { apiKey: "my-secret-key", }; // config.apiKey = "hacked"; // ❌ 에러 발생 (읽기 전용 속성이므로 할당 불가) readonly를 사용하면 의도치 않게 중요한 데이터가 수정되는 것을 코드 레벨에서 방지할 수 있다. --- 요약 1. object 타입: 단순히 "객체임"만 명시하므로 프로퍼티 접근이 불가능하다. 지양하는 것이 좋다. 2. 객체 리터럴 타입: { key: type } 형태로 구조를 명확히 정의한다. (구조적 타이핑) 3. 선택적 프로퍼티 (?): 해당 프로퍼티가 없어도 에러가 나지 않는다. 4. 읽기 전용 프로퍼티 (readonly): 초기화 이후 값을 수정할 수 없다.

TypeScriptStudy기초문법Object
TypeScript

[TypeScript] 배열(Array)과 튜플(Tuple)

November 26, 2025

이번에는 타입스크립트의 배열(Array) 과 튜플(Tuple) 타입에 대해 살펴본다. 배열은 자바스크립트의 배열과 크게 다르지 않지만, 튜플은 자바스크립트에는 없는 타입스크립트만의 특별한 타입이다. 1. 배열 (Array) 타입스크립트에서 배열 타입을 정의하는 방법은 크게 두 가지가 있다. 1-1. 기본 타입 정의 (type[]) 가장 일반적인 방법은 변수명 뒤에 타입[] 형태로 정의하는 것이다. ts let numArr: number[] = [1, 2, 3]; let strArr: string[] = ["hello", "im", "taemmi"]; 1-2. 제네릭 문법 (Array<type>) Array<배열요소타입> 형태로도 정의할 수 있다. 꺾쇠(< >)와 함께 타입을 작성하는 것을 제네릭(Generic) 이라고 부른다. ts let boolArr: Array<boolean> = [true, false, true]; > Note: 두 방식은 기능상 차이가 없으나, 보통 타이핑이 더 간편한 첫 번째 방식(type[])을 주로 사용한다. > 1-3. 다양한 타입의 요소를 갖는 배열 (Union Type) 배열 안에 숫자와 문자열이 동시에 들어가야 한다면 어떻게 해야 할까? 이럴 때는 소괄호와 바(|)를 이용해 유니온(Union) 타입으로 정의하면 된다. ts // string 또는 number가 들어갈 수 있는 배열 let multArr: (string | number)[] = [1, "hello"]; - (string | number): 배열의 요소가 문자열이거나 숫자여야 함을 의미한다. 1-4. 다차원 배열 배열 안에 배열이 들어가는 2차원 이상의 배열은 []를 연달아 작성하여 정의한다. ts let doubleArr: number[][] = [ [1, 2, 3], [4, 5], ]; --- 2. 튜플 (Tuple) 튜플은 자바스크립트에는 없는 개념으로, 길이와 각 요소의 타입이 고정된 배열을 의미한다. 2-1. 튜플 정의하기 일반 배열과 달리 대괄호 [] 안에 각 인덱스별로 들어갈 타입을 명시한다. ts // 길이가 2이고, 각각 number 타입인 튜플 let tup1: [number, number] = [1, 2]; // 길이가 3이고, 순서대로 number, string, boolean인 튜플 let tup2: [number, string, boolean] = [1, "2", true]; 이렇게 정의하면 정해진 순서와 타입, 그리고 길이를 엄격하게 지켜야 한다. 2-2. 튜플 사용 시 주의사항 (배열 메서드) 튜플은 컴파일되면 결국 자바스크립트의 배열(Array) 로 변환된다. 따라서 push나 pop 같은 배열 메서드를 사용하면 고정된 길이를 무시하고 요소를 추가하거나 삭제할 수 있다. ts let tup1: [number, number] = [1, 2]; // ❌ 에러를 뱉지 않음 (런타임에서는 배열이기 때문) tup1.push(1); tup1.push(1); 타입스크립트가 이런 메서드 호출까지 막아주지는 못하므로, 튜플을 쓸 때는 배열 메서드 사용에 각별히 주의해야 한다. 2-3. 튜플은 언제 쓸까? 예를 들어 회원 정보를 저장하는 2차원 배열이 있다고 가정해보자. [이름, 아이디] 순서로 저장하고 싶다. 일반 배열을 쓴다면 실수로 순서를 바꿔 넣어도 자바스크립트는 알아채지 못한다. ts // JS 방식 (실수 가능성 있음) const users = [ ["탬니", 1], ["홍길동", 2], [3, "아무개"], // ❌ 순서가 바뀌었지만 에러 없음 ]; 하지만 튜플을 사용하면 이러한 실수를 즉시 잡아낼 수 있다. ts // TS 튜플 사용 const users: [string, number][] = [ ["탬니", 1], ["홍길동", 2], // [3, "아무개"], // ❌ 컴파일 에러 발생! (순서 틀림) ]; 이처럼 데이터의 순서와 타입이 중요한 경우 튜플을 사용하면 훨씬 안전하게 코드를 작성할 수 있다. --- 요약 1. 배열: number[] 혹은 Array<number>로 정의한다. 유니온 타입을 써서 여러 타입을 담을 수도 있다. 2. 튜플: 길이와 타입이 고정된 배열이다 ([string, number]). 3. 주의: 튜플도 결국 배열이라 push 등의 메서드에는 뚫릴 수 있으니 주의해서 사용하자.

TypeScriptStudy기초문법ArrayTuple
TypeScript

[TypeScript] 원시 타입(Primitive Type)과 리터럴 타입(Literal Type)

November 26, 2025

TypeScript의 가장 기초가 되는 원시 타입(Primitive Type) 과 값 자체가 타입이 되는 리터럴 타입(Literal Type) 에 대해 정리해 본다. 1. 원시 타입 (Primitive Type) 원시 타입이란 동시에 하나의 값만 저장할 수 있는 타입을 의미한다. 배열이나 객체 같은 비원시 타입들이 동시에 여러 개의 값을 저장할 수 있는 것과 달리, number, string, boolean 등은 딱 하나의 값만 가질 수 있다. > 참고: 실습 시 tsconfig.json의 isolateModules 옵션이 true라면, 파일 내에 export 키워드가 하나 이상 있어야 에러가 발생하지 않는다. > 1-1. number 타입 자바스크립트에서 숫자를 의미하는 모든 값을 포함한다. 단순 정수뿐만 아니라 소수, 음수, 그리고 Infinity, NaN 같은 특수 값도 포함된다. ts // number let num1: number = 123; // 양수 let num2: number = -123; // 음수 let num3: number = 0.123; // 실수 let num4: number = -0.123; // 음의 실수 let num5: number = Infinity; // 무한대 let num6: number = -Infinity; // 음의 무한대 let num7: number = NaN; // Not a Number 💡 타입 주석 (Type Annotation) 변수명 뒤에 콜론(:)과 함께 타입을 정의하는 문법을 ‘타입 주석’ 또는 ‘타입 어노테이션’이라고 부른다. 주의사항number 타입으로 정의된 변수에는 문자열 등 다른 값을 넣을 수 없으며, 해당 타입에 맞지 않는 메서드(예: toUpperCase)는 사용할 수 없다. ts // 에러 발생 예시 // num1 = 'hello'; // ❌ Type 'string' is not assignable to type 'number'. // num1.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on type 'number'. 1-2. string 타입 문자열을 의미하는 타입이다. 큰따옴표("), 작은따옴표('), 백틱( ), 템플릿 리터럴을 모두 포함한다. ts // string let str1: string = "Hello"; let str2: string = 'Hello'; let str3: string = Hello; let str4: string = Hello ${num1}; // 템플릿 리터럴 1-3. boolean 타입 참(true)과 거짓(false)만을 저장하는 타입이다. ts // boolean let bool1: boolean = true; let bool2: boolean = false; 1-4. null과 undefined 타입 각각 오직 null 값과 undefined 값 하나만 포함하는 타입이다. ts // null let null1: null = null; // undefined let unde1: undefined = undefined; --- 2. strictNullChecks 옵션 (엄격한 null 검사) 자바스크립트에서는 변수를 초기화할 때 임시로 null을 넣어두곤 했다. 하지만 타입스크립트에서는 number 타입에 null을 넣으면 에러가 발생한다. null은 number 타입에 포함되지 않기 때문이다. ts let numA: number = null; // ❌ 기본적으로 에러 발생 만약 null을 다른 타입의 임시 값으로 허용하고 싶다면 tsconfig.json에서 설정을 변경해야 한다. tsconfig.json 설정 JSON { "compilerOptions": { "strictNullChecks": false, // ... } } - strictNullChecks: null 값을 변수에 할당하는 것을 엄격하게 검사할지 여부를 결정하는 옵션이다. - true (기본값): null 타입이 아닌 변수에는 null을 할당할 수 없다. (strict 옵션이 켜져 있으면 자동으로 켜진다.) - false: 모든 타입의 변수에 null을 자유롭게 할당할 수 있다. > Note: 안전한 타이핑을 위해 특별한 이유가 없다면 strictNullChecks는 true로 유지(또는 삭제하지 않음)하는 것이 좋다. > --- 3. 리터럴 타입 (Literal Type) 타입스크립트에는 string, number처럼 범용적인 타입 외에, 딱 하나의 값만 포함하는 타입도 존재한다. 이를 리터럴(값) 타입이라고 부른다. 변수의 타입을 특정 값 그 자체로 지정하면, 해당 변수는 그 값 이외의 어떤 값도 가질 수 없게 된다. ts // 숫자 리터럴 타입 let numA: 10 = 10; // numA = 12; // ❌ 10 이외의 값 할당 불가 // 문자열 리터럴 타입 let strA: "Hello" = "Hello"; // 불리언 리터럴 타입 let boolA: true = true; --- 요약 1. 원시 타입: 하나의 값만 저장한다 (number, string, boolean, null, undefined). 2. strictNullChecks: null 값을 엄격하게 관리하는 옵션이다. 안전한 코드를 위해 켜두는 것을 권장한다. 3. 리터럴 타입: 값 자체가 타입이 되어, 지정된 값만 허용하는 타입이다.

TypeScriptStudy기초문법Primitive TypeLiteral Type