3장. 고급 타입
타입스크립트만의 독자적 타입 시스템
-
any 타입
-
자바스크립트에 존재하는 모든 값을 오류 없이 받을 수 있음
-
즉, 타입을 명시하지 않은 것과 동일한 효과를 나타냄
⇒ any 타입의 사용을 지양하자. 타입스크립트의 타입 검사를 무색하게 만들어 잠재적으로 위험한 상황을 초래할 가능성이 커진다
-
any 타입을 어쩔 수 없이 사용해야 할 때
- 개발 단계에서 임시로 값을 지정해야 할 때
- 어떤 값을 받아올지 또는 넘겨줄지 정할 수 없을 때
- 값을 예측할 수 없을 때 암묵적으로 사용
-
-
unknown 타입
- any 타입을 제외한 다른 타입으로 선언된 변수에는 unknown 타입 값을 할당할 수 없음
any unknown 어떤 타입이든 any 타입에 할당 가능 어떤 타입이든 unknown 타입에 할당 가능 any 타입은 어떤 타입으로도 할당 가능 (단 never 제외) unknown 타입은 any 타입 외에 다른 타입으로 할당 불가능 let unknownValue: unknown; unknownValue = 100; // any 타입과 유사하게 숫자이든 unknownValue = "hello world"; // 문자열이든 unknownValue = () => console.log("this is any type"); // 함수이든상관없이할당이가능하지만 let someValue1: any = unknownValue; // (O) any 타입으로 선언된 변수를 제외한 다른 변수는 모두 할당이 불가 let someValue2: number = unknownValue; // (X) let someValue3: string = unknownValue; // (X)
- 무엇이 할당될지 아직 모르는 상태의 타입
- unknown 타입은 컴파일러가 아무런 경고를 주지 않지만, 이를 실행하면 에러가 발생함
let unknownValue: unknown; unknownValue = 100; // any 타입과 유사하게 숫자이든 unknownValue = "hello world"; // 문자열이든 unknownValue = () => console.log("this is any type"); // 함수이든상관없이할당이가능하지만 let someValue1: any = unknownValue; // (O) any 타입으로 선언된 변수를 제외한 다른 변수는 모두 할당이 불가 let someValue2: number = unknownValue; // (X) let someValue3: string = unknownValue; // (X)
- unknown 타입으로 선언된 변수는 값을 가져오거나 내부 속성에 접근할 수 없음
- 이는 unknown 타입으로 할당된 변수는 어떤 값이든 올 수 있음을 의미하는 동시에 개발자에게 엄격한 타입 검사를 강제하는 의도를 가짐
- any의 경우, 특정 타입으로 수정해야 하는 것을 깜빡하고 누락하면 어떤 값이든 전달될 수 있기 때문에 런타임에 예상치 못한 버그가 발생할 가능성이 높아짐
- 이러한 상황을 보완하기 위해 등장한 타입이 unknown
- any 타입과 유사하지만 타입 검사를 강제하고 타입이 식별된 후에 사용할 수 있기 때문에 any 타입보다 더 안전함
⇒ 데이터 구조를 파악하기 힘들 때 any 타입 대신 unknown 타입으로 대체해서 사용하는 방법이 권장됨
any를 사용하나요? 사용한다면 어떤 상황에서 사용할까요?
- 냥이팀
- 웬만해서 쓰지 않음
- 특정 타입을 어떻게 좁혀서 사용할지 모를 때 어쩔 수 없이 사용
- 외부에서 어떤 값이 들어올지 모르는 상황
- 그런데 깨진 유리창 이론처럼 any를 한 번 쓰기 시작하면 여기저기서 남용하게 될까봐 걱정됨.
- 다른 함수나 객체에 최대한 영향을 주지 않도록 관리하는 것이 중요하겠죠
- 감자팀
- 웬만해서 쓰지 않음
- 표 컴포넌트처럼 어떤 값을 받을지 모르는 상황에서 unknown을 사용하면, 가공할 때 타입 캐스팅을 모두 해야하는 상황이 생겨서 이런 경우 any 사용
unknown은 어떨 때 사용할 수 있을까요?
- 배달이팀
- 강제 타입 캐스팅을 통해 타입을 전환할 때 사용
const env = process.env as unknown as ProcessEnv
같은 식으로
- 왕팀
- 예상할 수 없는 데이터일 때
- try- catch 에러의 타입이 any에서 unknown으로 변경되어서 에러 핸들링할 때도 unknown 사용
- as unknown as Type과 같이 강제 타입 캐스팅을 하기도 하는데, 사실 이것도 any와 다를 바 없어서 지양해야 함
-
void 타입
-
never 타입
-
값을 반환할 수 없는 타입
-
값을 반환할 수 없는 타입의 예
- 에러를 던지는 경우
function generateError(res: Response): never { throw new Error(res.getMessage()); }
- 무한히 함수가 실행되는 경우
function checkStatus(): never { while (true) { // ... } }
-
-
Array 타입
- 여러 타입을 모두 관리해야 하는 배열을 선언하려면 유니온 타입을 활용할 수 있음
const array1: Array<number | string> = [1, "string"]; const array2: number[] | string[] = [1, "string"]; // 후자의 방식은 아래와 같이 선언할 수도 있다 const array3: (number | string)[] = [1, "string"];
- 타입스크립트에서 배열 타입을 명시하는 것만으로 배열의 길이까지는 제한할 수 없음
- 그러나 튜플은 배열 타입의 하위 타입으로 기존 타입스크립트의 배열 기능에 길이 제한까지 추가한 타입 시스템이라고 볼 수 있음
- 튜플은 타입스크립트의 타입 시스템과 대괄호를 사용해 선언할 수 있음
- 대괄호 안에 타입 시스템을 기술하는 것이 배열 타입과 유일하게 다른 점임
- 이때 대괄호 안에 선언하는 타입의 개수가 튜플이 가질 수 있는 원소의 개수를 나타냄
- 즉, 튜플은 배열의 특정 인덱스에 정해진 타입을 선언하는 것과 같음
let tuple: [number] = [1]; tuple = [1, 2]; // 불가능 tuple = [1, "string"]; // 불가능 let tuple: [number, string, boolean] = [1, "string", true]; // 여러 타입과 혼합도 가능하다
- 타입스크립트에서의 배열과 튜플은 자바스크립트와 달리 제한적으로 사용됨
- 배열 : 사전에 허용하지 않은 타입이 서로 섞이는 것을 방지 → 타입 안정성 제공
- 튜플 : 길이까지 제한하여 원소 개수와 타입을 보장
- 타입을 제한하는 것은 자바스크립트가 갖는 동적 언어의 자유로움으로 인해 발생할 수 있는 런타임 에러와 유지보수의 어려움을 막기 위한 것
- 특히 튜플의 경우 컨벤션을 잘 지키고 각 배열 원소의 명확한 의미와 쓰임을 보장할 때 더욱 안전하게 사용할 수 있는 타입
- 리액트 예시로, useState는 튜플 타입을 반환함. 반환 값이 명확하고 잘 설계된 API기 때문에 이와 같은 유용성을 얻을 수 있음
import { useState } from "react"; const [value, setValue] = useState(false); const [username, setUsername] = useState("");
- 튜플과 배열의 성질을 혼합해 사용할 수도 있음.
const httpStatusFromPaths: [number, string, ...string[]] = [ 400, "Bad Request", "/users/:id", "/users/:userId", "/users/:uuid", ]; // 첫 번째 자리는 숫자(400), 두 번째 자리는 문자열(‘Bad Request’)을 받아야 하고, 그 이후로는 문자열 타입의 원소를 개수 제한 없이 받을 수 있음
-
옵셔널 프로퍼티(선택적 속성)을 명시하고 싶다면 물음표(?) 기호와 함께 해당 속성을 선언할 수 있음
옵셔널 : 특정 속성 또는 매개변수가 값이 있을 수도 있고 없을 수도 있는 것을 의미함. 즉, 선택적 매개변수(옵셔널 파라미터) 또는 선택적 속성(옵셔널 프로퍼티)은 필수적으로 존재하지 않아도 되며 선택적으로 사용될 수 있음을 나타냄
-
enum 타입
- 열거형이라고도 부름
- 일종의 구조체를 만드는 타입 시스템
- 각각의 멤버를 가지고 있으며, 명명한 각 멤버의 값을 스스로 추론함.
- 기본적인 추론 방식은 숫자 0부터 1씩 늘려가며 값을 할당하는 것
- 각 멤버에 명시적으로 값을 할당할 수 있음
enum ProgrammingLanguage { Typescript, // 0 Javascript, // 1 Java, // 2 Python, // 3 Kotlin, // 4 Rust, // 5 Go, // 6 } // 각 멤버에게 접근하는 방식은 자바스크립트에서 객체의 속성에 접근하는 방식과 동일하다 ProgrammingLanguage.Typescript; // 0 ProgrammingLanguage.Rust; // 5 ProgrammingLanguage["Go"]; // 6 // 또한 역방향으로도 접근이 가능하다 ProgrammingLanguage[2]; // “Java” enum ProgrammingLanguage { Typescript = "Typescript", Javascript = "Javascript", Java = 300, Python = 400, Kotlin, // 401 Rust, // 402 Go, // 403 }
- 주로 문자열 상수를 생성하는 데 사용됨
- 그 자체로 변수 타입으로 지정 가능. 이때 열거형을 타입으로 가지는 변수는 해당 열거형이 가지는 모든 멤버를 값으로 받을 수 있음 ⇒ 코드의 가독성을 높임
enum ItemStatusType { DELIVERY_HOLD = "DELIVERY_HOLD", // 배송 보류 DELIVERY_READY = "DELIVERY_READY", // 배송 준비 중 DELIVERING = "DELIVERING", // 배송 중 DELIVERED = "DELIVERED", // 배송 완료 } const checkItemAvailable = (itemStatus: ItemStatusType) => { switch (itemStatus) { case ItemStatusType.DELIVERY_HOLD: case ItemStatusType.DELIVERY_READY: case ItemStatusType.DELIVERING: return false; case ItemStatusType.DELIVERED: default: return true; } };
- 열거형 사용의 장점
- 타입 안정성
- 명확한 의미 전달과 높은 응집력
- 가독성
- 다만 열거형에서 숫자로만 이루어져 있거나 타입스크립트가 자동으로 추론한 열거형은 안전하지 않은 결과를 낳을 수 있음
- 할당된 값을 넘어서는 범위로 역방향으로 접근하더라도 타입스크립트는 막지 않음
- 이러한 동작을 막기 위해 const enum으로 열거형을 선언하는 방법이 있음
- 이 방식은 역방향으로의 접근을 허용하지 않기 때문에 자바스크립트에서의 객체에 접근하는 것과 유사한 동작을 보장함
ProgrammingLanguage[200]; // undefined를 출력하지만 별다른 에러를 발생시키지 않는다 // 다음과 같이 선언하면 위와 같은 문제를 방지할 수 있다 const enum ProgrammingLanguage { // ... }
- 그러나 const enum으로 열거형을 선언하더라도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 방지하지 못함
- 반면 문자열 상수 방식으로 선언한 열거형은 미리 선언하지 않은 멤버로 접근을 방지
⇒ 문자열 상수 방식으로 열거형을 사용하는 것이 더 안전하며 의도하지 않은 값의 할당이나 접근을 방지하는 데 도움이 됨
const enum NUMBER { ONE = 1, TWO = 2, } const myNumber: NUMBER = 100; // NUMBER enum에서 100을 관리하고 있지 않지만 이는 에러를 발생시키지 않는다 const enum STRING_NUMBER { ONE = "ONE", TWO = "TWO", } const myStringNumber: STRING_NUMBER = "THREE"; // Error
- 또한 열거형은 일부 번들러에서 트리쉐이킹 과정 중 즉시 실행 함수로 변환된 값을 사용하지 않는 코드로 인식하지 못하는 경우가 발생할 수 있음.
- 따라서 불필요한 코드의 크기가 증가하는 결과가 발생할 수 있음
- 이러한 문제를 해결하기 위해 const enum 또는 as const assertion을 사용해 유니온 타입으로 열거형과 동일한 효과를 얻는 방법이 있음
타입 조합
-
교차 타입(Intersection)
- 기존에 존재하는 다른 타입들을 합쳐서 해당 타입의 모든 멤버를 가지는 새로운 타입을 생성하는 것
&
을 사용해 표기 ⇒A&B
: 타입 A와 타입 B를 모두 만족- 결과물로 탄생한 단일 타입에는 타입 별칭(type alias)를 붙일 수 있음
-
유니온 타입(Union)
|
를 사용해 표기 ⇒A | B
: 타입 A와 타입 B 중 하나가 될 수 있는 타입- 특정 변수가 가질 수 있는 타입을 전부 나열하는 용도로 사용됨
- 2개 이상의 타입을 이어 붙일 수 있고 타입 별칭을 통해 중복을 줄일 수 있음
-
인덱스 시그니처(Index Signatures)
- 특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 있을 때 사용하는 문법
- 인터페이스 내부에
[Key: K]: T
꼴로 타입을 명시 - 이는 해당 타입의 속성 키는 모두 K 타입이어야 하고 속성 값은 모두 T 타입을 가져야 한다는 의미임
- 다른 속성을 추가로 명시해줄 수 있는데 이때 추가로 명시된 속성은 인덱스 시그니처에 포함되는 타입이어야 함
// 인덱스 시그니처의 키가 string일 때는 number | boolean 타입이 오게끔 선언됨 interface IndexSignatureEx2 { [key: string]: number | boolean; length: number; isValid: boolean; name: string; // 에러 발생 }
-
인덱스드 엑세스 타입(Indexed Access Types)
- 다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용됨
- 인덱스에 사용되는 타입 또한 그 자체로 타입이기 때문에 유니온 타입, keyof, 타입 별칭 등의 표현을 사용할 수 있음
type Example = { a: number; b: string; c: boolean; }; type IndexedAccess = Example["a"]; type IndexedAccess2 = Example["a" | "b"]; // number | string type IndexedAccess3 = Example[keyof Example]; // number | string | boolean type ExAlias = "b" | "c"; type IndexedAccess4 = Example[ExAlias]; // string | boolean
- 배열의 요소 타입을 조회하기 위해 인덱스드 엑세스 타입을 사용하는 경우가 있음
- 배열 타입의 모든 요소는 전부 동일한 타입을 가지며 배열의 인덱스는 숫자 타입임.
- 따라서 number로 인덱싱하여 배열 요소를 얻은 다음에 typeof 연산자를 붙여주면 해당 배열 요소의 타입을 가져올 수 있음
const PromotionList = [ { type: "product", name: "chicken" }, { type: "product", name: "pizza" }, { type: "card", name: "chee-up" }, ]; type ElementOf<T> = (typeof T)[number]; // type PromotionItemType = { type: string; name: string } type PromotionItemType = ElementOf<PromotionList>;
-
맵드 타입(Mapped Types)
- 다른 타입을 기반으로 한 타입을 선언할 때 사용하는 문법
- 인덱스 시그니처 문법을 사용해서 반복적인 타입 선언을 효과적으로 줄일 수 있음
type Example = { a: number; b: string; c: boolean; }; type Subset<T> = { [K in keyof T]?: T[K]; }; const aExample: Subset<Example> = { a: 3 }; const bExample: Subset<Example> = { b: "hello" }; const acExample: Subset<Example> = { a: 4, c: true };
- 맵드 타입에서 매핑할 때는
readonly
와?
을 수식어로 적용할 수 있음 - 맵드 타입은 이러한 수식어를 더해주는 것뿐만 아니라 제거할 수도 있음
- 기본 타입에 존재하던 readonly나 ? 앞에
-
를 붙여주면 해당 수식어를 제거한 타입을 선언할 수 있음
type ReadOnlyEx = { readonly a: number; readonly b: string; }; type CreateMutable<Type> = { -readonly [Property in keyof Type]: Type[Property]; }; type ResultType = CreateMutable<ReadOnlyEx>; // { a: number; b: string } type OptionalEx = { a?: number; b?: string; c: boolean; }; type Concrete<Type> = { [Property in keyof Type]-?: Type[Property]; }; type ResultType = Concrete<OptionalEx>; // { a: number; b: string; c: boolean }
- 실제 예시 : 배민 선물하기 서비스 ‘바텀시트’ 컴포넌트
const BottomSheetMap = { RECENT_CONTACTS: RecentContactsBottomSheet, CARD_SELECT: CardSelectBottomSheet, SORT_FILTER: SortFilterBottomSheet, PRODUCT_SELECT: ProductSelectBottomSheet, REPLY_CARD_SELECT: ReplyCardSelectBottomSheet, RESEND: ResendBottomSheet, STICKER: StickerBottomSheet, BASE: null, }; export type BOTTOM_SHEET_ID = keyof typeof BottomSheetMap; // type BOTTOM_SHEET_ID = "RECENT_CONTACTS" | "CARD_SELECT" | "SORT_FILTER" | "PRODUCT_SELECT" | "REPLY_CARD_SELECT" | "RESEND" | "STICKER" | "BASE" // 불필요한 반복이 발생한다 type BottomSheetStore = { RECENT_CONTACTS: { resolver?: (payload: any) => void; args?: any; isOpened: boolean; }; CARD_SELECT: { resolver?: (payload: any) => void; args?: any; isOpened: boolean; }; SORT_FILTER: { resolver?: (payload: any) => void; args?: any; isOpened: boolean; }; // ... }; // Mapped Types를 통해 효율적으로 타입을 선언할 수 있다 type BottomSheetStore = { [index in BOTTOM_SHEET_ID]: { resolver?: (payload: any) => void; args?: any; isOpened: boolean; }; };
- 덧붙여 맵드 타입에서는 as 키워드를 사용하여 키를 효율적으로 재지정할 수 있음
// as 키워드 사용 전 type BottomSheetStore = { [index in BOTTOM_SHEET_ID]: { resolver?: (payload: any) => void; args?: any; isOpened: boolean; }; }; /** type BottomSheetStore = { RECENT_CONTACTS: { resolver?: ((payload: any) => void) | undefined; args?: any; isOpened: boolean; }; ... 7 more ...; } */ // as 키워드 사용 후 type BottomSheetStore = { [index in BOTTOM_SHEET_ID as `${index}_BOTTOM_SHEET`]: { resolver?: (payload: any) => void; args?: any; isOpened: boolean; }; }; /** type BottomSheetStore = { RECENT_CONTACTS_BOTTOM_SHEET: { resolver?: ((payload: any) => void) | undefined; args?: any; isOpened: boolean; }; ... 7 more ...; } */
-
템플릿 리터럴 타입(Template Literal Types)
- 자바스크립트 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법
- 위 예제에서 BottomSheetStore의 각 키에
_BOTTOM_SHEET
를 붙여주는 예시가 바로 템플릿 리터럴 타입을 활용한 것
type Stage = | "init" | "select-image" | "edit-image" | "decorate-card" | "capture-image"; type StageName = `${Stage}-stage`; // ‘init-stage’ | ‘select-image-stage’ | ‘edit-image-stage’ | ‘decorate-card-stage’ | ‘capture-image-stage’
-
제네릭(Generic)
- 일반화된 데이터 타입을 의미
- 정적 언어에서 다양한 타입 간에 재사용성을 높이기 위해 사용하는 문법
- 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워 둔 다음에, 실제로 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식
- 여러 타입에 대해 하나하나 따로 정의하지 않아도 됨 ⇒ 재사용성 매우 향상
- 타입 변수는 일반적으로
<T>
와 같이 꺽쇠괄호 내부에 정의되며, 사용할 때는 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣어주면 됨 - 보통 타입 변수명으로 T(Type), E(Element), K(Key), V(Value) 등 한 글자로 된 이름을 많이 사용함
type ExampleArrayType<T> = T[]; const array1: ExampleArrayType<string> = ["치킨", "피자", "우동"];
제네릭과 any는 명확히 다르다
- 둘의 차이는 배열을 보면 쉽게 알 수 있다.
- any 타입의 배열에서는 배열 요소들의 타입들이 전부 같지 않을 수 있다. 즉, 타입 검사를 하지 않고 모든 타입이 허용되는 타입으로 취급된다.
- 제네릭은 배열 생성 시점에 원하는 타입으로 특정할 수 있다. 즉, 배열 요소가 전부 동일한 타입이라고 보장할 수 있다.
- 제네릭 함수를 호출할 때 타입 추론이 가능한 경우에는 타입 명시를 생략할 수 있음
- 특정 요소 타입을 알 수 없을 때는 제네릭 타입에 기본값을 추가할 수 있음
interface SubmitEvent<T = HTMLElement> extends SyntheticEvent<T> { submitter: T; }
- 특정 타입에서만 존재하는 멤버를 참조하려고 하면 안됨. 컴파일러는 어떤 타입이 제네릭에 전달될지 알 수 없기 때문
// 배열에만 존재하는 length 속성을 제네릭에서 참조하려고 해 에러가 발생한 상황 function exampleFunc2<T>(arg: T): number { return arg.length; // 에러 발생: Property ‘length’ does not exist on type ‘T’ }
- 이럴 때는 제네릭 꺽쇠괄호 내부에 ‘length 속성을 가진 타입만 받는다’라는 제약을 걸어줌으로써 length 속성을 사용할 수 있게끔 만들 수 있음
interface TypeWithLength { length: number; } function exampleFunc2<T extends TypeWithLength>(arg: T): number { return arg.length; }
주의: 제네릭 사용 시, 파일 확장자가 tsx일 때 꺽쇠괄호를 혼동해 에러가 발생한다.
- 이러한 상황을 피하기 위해 제네릭 부분에 extends 키워드를 사용하여 컴파일러에게 특정 타입의 하위 타입만 올 수 있음을 확실히 알려주면 된다.
- 보통 제네릭을 사용할 때는 function 키워드로 선언하는 경우가 많다.
// 에러 발생: JSX element ‘T’ has no corresponding closing tag const arrowExampleFunc = <T>(arg: T): T[] => { return new Array(3).fill(arg); }; // 에러 발생 X const arrowExampleFunc2 = <T extends {}>(arg: T): T[] => { return new Array(3).fill(arg); };
제네릭 사용법
-
함수의 제네릭
- 어떤 함수의 매개변수나 반환 값에 다양한 타입이 넣고 싶을 때 사용 가능
function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string): Repository<T> { return getConnection(“ro”).getRepository(target); }
-
호출 시그니처의 제네릭
호출 시그니처 : 타입스크립트의 함수 타입 문법으로 함수의 매개변수와 반환 타입을 미리 선언하는 것
- 호출 시그니처를 사용함으로써 개발자는 함수 호출 시 필요한 타입을 별도로 지정할 수 있음
- 호출 시그니처를 사용할 때 제네릭 타입을 어디에 위치시키는지에 따라 타입의 범위와 제네릭 타입을 언제 구체 타입으로 한정할지를 결정할 수 있음
interface useSelectPaginationProps<T> { categoryAtom: RecoilState<number>; filterAtom: RecoilState<string[]>; sortAtom: RecoilState<SortType>; fetcherFunc: (props: CommonListRequest) = > Promise<DefaultResponse<ContentListRes ponse<T>>>; }
export type UseRequesterHookType = <RequestData = void, ResponseData = void>( baseURL?: string | Headers, defaultHeader?: Headers ) => [RequestStatus, Requester<RequestData, ResponseData>];
function useSelectPagination< T extends CardListContent | CommonProductResponse >({ categoryAtom, filterAtom, sortAtom, fetcherFunc, }: useSelectPaginationProps<T>): { intersectionRef: RefObject<HTMLDivElement>; data: T[]; categoryId: number; isLoading: boolean; isEmpty: boolean; } { // ... return { intersectionRef, data: swappedData ?? [], isLoading, categoryId, isEmpty, }; }
-
제네릭 클래스
제네릭 클래스 : 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스
- 클래스 이름 뒤에 타입 매개변수인
<T>
를 선언함 <T>
는 메서드의 매개변수나 반환 타입으로 사용될 수 있음- LocalDB 클래스는 외부에서
{ key: string; value: Promise<Record<string, unknown>>; cacheTTL: number }
타입을 받아들여 클래스 내부에서 사용될 제네릭 타입으로 결정됨
class LocalDB<T> { // ... async put(table: string, row: T): Promise<T> { return new Promise<T>((resolved, rejected) = > { /* T 타입의 데이터를 DB에 저장 */ }); } async get(table:string, key: any): Promise<T> { return new Promise<T>((resolved, rejected) = > { /* T 타입의 데이터를 DB에서 가져옴 */ }); } async getTable(table: string): Promise<T[]> { return new Promise<T[]>((resolved, rejected) = > { /* T[] 타입의 데이터를 DB에서 가져옴*/ }); } } export default class IndexedDB implements ICacheStore { private _DB?: LocalDB<{ key: string; value: Promise<Record<string, unknown>>; cacheTTL: number }>; private DB() { if (!this._DB) { this._DB = new LocalDB(“localCache”, { ver: 6, tables: [{ name: TABLE_NAME, keyPath: “key” }] }); } return this._DB; } // ... }
- 제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용됨
- 특정 메서드만을 대상으로 제네릭을 적용하려면 해당 메서드를 제네릭 메서드로 선언하면 됨
- 클래스 이름 뒤에 타입 매개변수인
-
제한된 제네릭
제한된 제네릭 : 타입 매개변수에 대한 제약 조건을 설정하는 기능
- ex) string 타입으로 제약하려면 타입 매개변수는 특정 타입을 상속(extends)해야 함
type ErrorRecord<Key extends string> = Exclude<Key, ErrorCodeType> extends never ? Partial<Record<Key, boolean>> : never;
- 이처럼 타입 매개변수가 특정 타입으로 묶였을 때(bind) 키를 바운드 타입 매개변수라고 부름
- 그리고 string을 키의 상한 한계라고 함
- 상속받을 수 있는 타입으로는 기본 타입뿐만 아니라 상황에 따라 인터페이스나 클래스도 사용할 수 있음
- 또한 유니온 타입을 상속해서 선언할 수도 있음
-
확장된 제네릭
- 제네릭 타입은 여러 타입을 상속받을 수 있으며 타입 매개변수를 여러 개 둘 수도 있음
- 제네릭의 유연성을 잃지 않으면서 타입을 제약해야 할 때는 타입 매개변수에 유니온 타입을 상속해서 선언하면 됨
<Key extends string | number>
- 유니온 타입으로 T가 여러 타입을 받게 할 수 있지만, 타입 매개변수가 여러 개일 때는 처리할 수 없음. 이럴 때는 매개변수를 하나 더 추가하여 선언한다.
// Ok 타입이나 Err 타입을 매개변수 인자로 받아 사용하는 예시 export class APIResponse<Ok, Err = string> { private readonly data: Ok | Err | null; private readonly status: ResponseStatus; private readonly statusCode: number | null; constructor( data: Ok | Err | null, statusCode: number | null, status: ResponseStatus ) { this.data = data; this.status = status; this.statusCode = statusCode; } public static Success<T, E = string>(data: T): APIResponse<T, E> { return new this<T, E>(data, 200, ResponseStatus.SUCCESS); } public static Error<T, E = unknown>(init: AxiosError): APIResponse<T, E> { if (!init.response) { return new this<T, E>(null, null, ResponseStatus.CLIENT_ERROR); } if (!init.response.data?.result) { return new this<T, E>( null, init.response.status, ResponseStatus.SERVER_ERROR ); } return new this<T, E>( init.response.data.result, init.response.status, ResponseStatus.FAILURE ); } // ... } // 사용하는 쪽 코드 const fetchShopStatus = async (): Promise< APIResponse<IShopResponse | null> > => { // ... return (await API.get<IShopResponse | null>("/v1/main/shop", config)).map( (it) => it.result ); };
-
제네릭 예시
- 장점 : 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용, 가독성을 높임
- 현업에서 가장 많이 제네릭이 활용할 때 : API 응답 값의 타입을 지정할 때
// API 응답 값에 따라 달라지는 data를 제네릭 타입 Data로 선언 export interface MobileApiResponse<Data> { data: Data; statusCode: string; statusMessage?: string; } export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => { const priceUrl = "https: ~~~"; // url 주소 return request({ method: "GET", url: priceUrl, }); }; export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => { const orderUrl = "https: ~~~"; // url 주소 return request({ method: "GET", url: orderUrl, }); };
- 필요하지 않은 상황에 제네릭을 사용해, 독이 되는 사례
-
제네릭을 굳이 사용하지 않아도 되는 타입
type GType<T> = T; type RequirementType = "USE" | "UN_USE" | "NON_SELECT"; interface Order { getRequirement(): GType<RequirementType>; }
- GType이 다른 곳에서는 사용되지 않고, getRequirement 함수의 반환 값 타입으로만 사용됨
- 위 코드는 굳이 제네릭을 사용하지 않고 타입 매개변수를 그대로 선언하는 것과 같은 기능을 하고 있음
type RequirementType = "USE" | "UN_USE" | "NON_SELECT"; interface Order { getRequirement(): RequirementType; }
-
any 사용하기
- any 사용 시 제네릭의 장점(높은 코드 재사용성)과 타입 추론 및 타입 검사를 할 수 있는 이점을 누릴 수 없음
- any 타입은 모든 타입을 허용하기 때문에, 타입을 지정하는 의미가 사라지게 됨
-
가독성을 고려하지 않은 사용
- 과한 제네릭의 사용은 가독성을 해침
- ⇒ 복잡한 제네릭은 의미 단위로 분할해서 사용하기
-