FrontEnd

2개의 게시글

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] 객체 타입의 호환성과 초과 프로퍼티 검사

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