
타입스크립트는 자바스크립트 자료형에서 제시되지 않은 독자적인 타입 시스템을 가지고 있지만 타입스크립트의 타입 시스템이 내포한 개념은 모두 자바스크립트에서 기인한 것이다.
이 장에서 소개하는 모든 타입 시스템은 타입스크립트에만 존재하는 키워드지만, 그 개념은 자바스크립트에 기인한 타입 시스템이라는 점을 인지하고 각 타입을 살펴보자.
자바스크립트에 존재하는 모든 값을 오류없이 받을 수 있다.
타입을 명시하지 않은 것과 동일한 효과를 나타낸다.
아래와 같이 any로 지정한 타입은 어떠한 값을 할당하더라도 오류가 발생하지 않는다.
let state:any;
// 객체 할당
state = {value:0};
// 숫자 할당
state = 100;
//문자열 할당
state = "hello world";
// 중첩구조로 들어가 함수 할당
state.inner = () => console.log("any type")
타입스크립트를 사용한 정적 타이핑의 의미를 무색하게 만들 수 있음
따라서 any 타입을 변수에 할당하는 것은 지양해야하는 패턴
하지만 타입스크립트에서 any타이블 어쩔 수없이 사용해야하는 3가지의 대표적인 경우 존재
개발단계에서 임시로 값을 지정해야 할 때
매우 복잡한 구성 요소로 이루어진 개발 과정에서 추후 값이 변경될 여지가 있거나 타입에 대한 확정이 아직 이루어지지 않을 경우 any를 사용할 수 있다. 다만 타입안전성을 해칠 위험이 있으므로 세부 스펙이 나오는 시점에서 타입을 대체하는 경우가 많으며 이 과정이 누락될 경우 문제가 발생할 수 있으므로 주의해야 한다.
어떤 값을 받아올지 또는 넘겨줄지 정할 수 없을 때
API 요청 및 응답처리, 콜백 함수 전달 등 어떤 값의 타입을 명확하게 정하기 힘든 경우 any타입을 사용할 수 있다.
값을 예측할 수 없을 때 암묵적으로 사용
외부 라이브러리나 웹 API요청에 의해 다양한 값을 반환하는 API가 존재할 수 있다. 브라우저의 Fetch API의 일부 메서는 요청 이후의 응답을 특정 포맷으로 파싱하는데 이대 반환 타입이 any로 매핑되어 있는 것을 확인 할 수 있다.
unknown타입은 any 타입과 유사하게 모든 타입의 값이 할당될 수 있다.
다만 차이점으로 any 는 다른 타입으로 선언된 변수에 any 타입 값을 할당할 수 있지만 unknown은 any 타입을 제외한 다른 타입으로 선언된 변수에는 unknown 타입 값을 할당할 수 없다.
let anyData:any;
anyData = "string"
anyData = () => {console.log("function")}
anyData = 100;
let anyData2:string = anyData; // 할당 가능
```
```ts
let unknownData: unknown;
unknownData = 100;
unknownData = 'string';
unknownData = () => {console.log('function')};
let unknownData3: any = unknownData; // 할당 가능
let unknownData2: string = unknownData; // "'unknown' 형식은 'string' 형식에 할당할 수 없습니다." 에러 발생
```
unknown 타입은 어떤 타입이 할당되었는지 알 수 없음을 나타내기 때문에 unknown타입으로 선언된 변수는 값을 가져오거나 내부 속성에 접근할 수 없음
unknown 타입으로 할당된 변수는 어떤 값이든 올 수 있음을 의미하는 동시에 개발자에게 엄격한 타입 검사를 강제
any 타입과 유사하지만 타입 검사를 강제하고 타입이 식별된 후에 사용할 수 있기 때문에 any 타입보다 더 안전
데이터 구조를 파악하기 힘든 경우 any 타입보다 unknown 타입으로 대체해서 사용하는 방식을 권장
자바스크립트 함수에서 명시적인 반환문을 작성하지 않으면 undefined가 반환된다.
하지만 타입스크립트에서 함수가 어떤 값을 반환하지 않을 경우에는 void를 지정하여 사용한다.
function logFunc(data: string): void {
console.log(data)
// return 없음
}
// 화살표 함수
const logFuncArrow = (data:string): void =>{
console.log(data)
// return 없음
}
일반적으로 함수 자체를 다른 함수의 인자로 전달하는 경우가 아니라면 void 타입은 잘 명시되지 않는 경향이 있다. 함수내부의 별도 반환문이 없을 경우 타입스크립트 컴파일러가 void로 타입을 추론해주기 때문이다.
void는 주로 함수의 반환 타입으로 사용하지만 함수에 국한된 타입은 아니다. 다만 함수가 아닌 값에 대해서는 대부분 무의미하다.
void로 타입이 지정된 변수는 undefined 또는 null만 할당할 수 있다.
그런데 만약 tsconfig.json에서 strictNullchecks옵션이 설정 되어 있거나 해당 플래그 설정이 실행되면 null값을 할당할 수 없다.
또한 명시적인 관점에서도 undefined나 null을 직접사용해서 지정하는 것이 더 바람직하다.
never 타입도 void와 마찬가지로 타입의 값을 반환할 수 없은 타입을 말한다. 보통은 에러를 던지는 경우와 무한히 함수가 실행되는 경우에 사용된다.
// 에러를 던지는 경우
function generateError(res:Response): never{
throw new Error(res.getMessage())
}
// 무한히 함수 실행
function checksStatus(): never{
while(true){
//...
}
}
추가로 void와 never의 차이를 말하자면 함수의 완료여부에 달려있다. 에러를 던지거나 무한히 함수가 실행되는 것은 함수 자체가 끝난 것이 아니다.
실행중이거나 중단된 시점, 이때 사용하는 것이 never다. void와 마찬가지로 반환값은 없지만 함수의 실행이 완료되었는지를 중심으로 생각하면 never와 void의 차이를 구분하는데 조금 더 도움이 될 것이다.
자바스크립트에서 배열은 객체에 속하는 타입으로 분류하며 단독으로 배열이라는 자료형에 국한하지 않는다.
타입스크립트에서 Array라는 타입을 사용하기 위해서는 타입스크립트의 특수한 문법을 사용해야한다.
자바스크립트의 배열은 동적 언어의 특징에 따라 어떤 값이든 배열의 원소로 허용한다.
하지만 이러한 개념은 타입스크립트의 정적 타이핑과 부합하지 않는다.
대개 정적타입의 언어에서는 배열을 선언할 때 크기까지 동시에 제한하기도 한다. 자바, C++ 같은 다른 정적 언어에서도 배열의 원소로 하나의 타입만 사용하도록 명시한다.
타입스크립트에서는 배열의 크기까지 제한하지는 않지만 정적 타입의 특성을 살려 명시적인 타입을 선언함으로서 해당 타입의 원소를 관리하는 것을 강제한다.
선언하는 방식으로는 크게 두가지가 있다.
const array:number[] = [1,2,3];const array:Array<number> = [1,2,3];만약 여러 타입을 모두 관리해야 하는 배열을 선언하고 싶은 경우에는 유니온 타입을 사용할 수 있다.
const array1: Array<number | string> = [1,"string"];
const array2: number[] | string[] = [1,"string"];
const array3: (number | string)[] = [1,"string"];
튜플은 배열 기능에 길이 제한까지 추가한 타입 시스템이다.
대괄호와 타입시스템을 사용하여 선언할 수 있으며 대괄호 안에 선언하는 타입의 개수가 튜플이 가질 수 있는 원소의 개수를 나타낸다.
let tuple: [number] = [1]; // O, 가능
tuple = [1,2]; // X, 불가능
tuple = [1,"string"]; // X, 불가능
let tuple2: [number,string,boolean] = [1, "hello world", true]; // O, 가능
배열은 사전에 허용하지 않은 타입이 서로 섞이는 것을 방지하여 타입 안전성을 제공한다.
튜플은 여기에 길이까지 제한하여 원소의 개수와 타입을 보장한다.
이처럼 타입을 제한하는 것은 자바스크립트의 런타임 에러와 유지 보수의 어려움을 막기 위한 것이며 특히 튜플의 경우 컨벤션을 잘 지키고 각 배열 원소의 명확한 의미와 쓰임을 보장할 때 더욱 안전하게 사용할 수 있다.
enum은 열거형이라고 부르는데 일종의 구조체를 만드는 타입 시스템이다.
enum을 사용하여 열거형을 정의할 수 있으며 각각의 멤버를 가진다.
이때 타입스크립트는 명명한 각 멤버의 값을 스스로 추론한다.
기본적인 추론방식은 숫자 0부터 1씩 늘려가며 값을 할당하는 것이다.
enum ProgrammingLanguage {
Typescript, // 0
Javascript, // 1
Java, // 2
Python, // 3
Rust // 4
}
// ProgrammingLanguage의 멤버로 접근
ProgrammingLanguage.Typescript; // 0
ProgrammingLanguage.Javascript; // 1
ProgrammingLanguage.Python; // 3
ProgrammingLanguage["Rust"]; // 4
// 역방향 접근도 가능
ProgrammingLanguage[2]; // Java
명시적인 값의 할당도 가능하며 일부 멤버에 값을 할당하지 않아도 누락된 멤버를 아래와 같은 방식으로 이전 멤버를 기준으로 1씩 늘려가며 자동으로 할당한다.
enum ProgrammingLanguage {
Typescript = "TypeScript",
Javascript = "JavaScript",
Java = 300,
Python = 400,
Rust, // 401
Go // 402
}
enum은 주로 문자열 상수를 생성하는데 사용되며 이를 통해 응집력 있는 집합 구조체를 만들어 사용자 입장에서 간편하게 활용할 수 있다.
또한 그 자체로 변수 타입으로 지정할 수 있으며 열거형을 타입으로 가지는 변수는 해당 열거형이 가지는 모든 멤버들을 값으로 받을 수 있다.
다음과 같이 itemStatus라는 인자가 ItemStatusType라는 열거형을 타입으로 가지면 문자열로 타입이 지정되었을때와 비교하여 다음과 같은 효과가 있다.
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;
}
};
타입안전성: ItemStatusType에 명시되지 않은 다른 문자열은 인자로 받을 수 없어 타입 안전성이 우수하다.
명확한 의미 전달과 높은 응집력: ItemStatusType이 다루는 값이 무엇인지 명확하고 아이템 상태에 대한 값을 모아놓은 것으로 응집력이 우수하다.
가독성: 응집도가 높기 때문에 말하고자 하는 바가 더욱 명확하다. 열거형 멤버를 통해 어떤 상태를 나타내는지 쉽게 알 수 있다.
이처럼 열거형은 관련이 높은 멤버를 모아 문자열 상수처럼 사용하고자 할 때 유용하게 사용할 수 있다.
의도하지 않은 값의 할당이나 접근
숫자로만 이루어져 있거나 타입스크립트가 자동으로 추론한 열거형은 안전하지 않은 결과를 낳을 수 있다. ex ) 할당된 값을 넘어서는 범위에 역방향으로 접근 가능
이러한 접근을 막기 위해 cosnt enum으로 열거형을 선언하는 방법이 있다. 이 방식은 역방향으로의 접근을 허용하지 않는다.
다만, const enum으로 열거형을 선언하더라도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 막지 못한다. 반면 문자열 상수로 관리되는 열거형은 의도하지 않은 값의 할당이나 접근을 방지하므로 문자열 상수로 관리하는 것이 도움이 된다.
불필요한 코드의 크기 증가
열거형은 타입공간과 값 공간에 모두 사용된다. 열거형은 TS에서 JS로 변환되며 즉시실행함수 형식으로 변환된다.
이때 즉시 실행 함수로 변환된 값을 사용하지 않는 코드로 인식하지 못하는 경우가 발생하여 불필요한 코드의 크기가 증가하는 결과를 초래할 수 있다.
이러한 문제를 해결하기 위해 앞서 언급한 cosnt enum 또는 as cosnt assertion을 사용하여 유니온 타입으로 열거형과 동일한 효과를 얻는 방법이 있다.
교차타입은 & 을 사용하여 표기하며 여러가지 타입을 결합하여 하나의 단일 타입으로 지정할때 사용한다.
결과물로 탄생한 단일 타입에는 타임 별칭을 붙일 수 있다.
type ProductItem = {
id:number;
name:string;
price:number;
};
type ProductItemWidtDiscount = ProductItem & {discountAmount: number};
// ProductItemWidtDiscount의 구성요소
// {
// id: number;
// name: string;
// price: number;
// discountAmount: number;
// };
유니온 타입은 A 또는 B 중 하나가 될 수 있는 타입을 말하며 |을 사용하여 표기한다. 특정 변수가 가질 수 있는 타입을 전부 나열하는 용도로 주로 사용한다. 교차타입과 마찬가지로 두개 이상의 타입을 이을 수 있고 타입 별칭을 사용할 수 있다.
아래 printPromotionItem() 함수는 name에는 접근이 가능하지만 price는 컴파일 에러가 뜬다. 그 이유는 ProductItem에만 price가 존재하기 때문이다. 만약 공통 속성이 아닌 price나 type같은 속성에 접근하고 싶을경우에는 타입가드를 사용하여 타입을 검증 한 뒤 접근해야 한다.
type ProductItem = {
id:number;
name:string;
price:number;
};
type CardItem = {
id:number;
name:string;
type:string;
};
type PromotionEventItem = ProductItem | CardItem;
const printPromotionItem = () => {
console.log(item.name) // O
console.log(item.price) // X, 컴파일 에러 발생
}
인덱스 시그니처는 특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 싶을 때 사용한다. 인터페이스 내부에 [key: K]: T꼴로 타입을 명시해주면 된다. 이는 해당 타입의 속성 키는 모두 K타입이며 value는 T타입을 가져야 한다는 의미다.
// 기본 형태
interface IndexSignatureEx {
[key:string]: number; // key의 이름은 문자열, value는 숫자형
}
아래 예시에서 name은 string을 가져오도록 되어있지만 인덱스 시그니처에서 key가 string일 경우에는 number | boolean이 오게끔 선언되어 있어 에러가 발생한다.
// 기본 형태
interface IndexSignatureEx2 {
[key:string]: number | boolean; // key의 이름은 문자열, value는 숫자형
length: number; // O
isOpen: boolean; // O
name: string; // X, 에러 발생
}
다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용되며 속성의 이름을 알 수 없거나 동적으로 속성의 이름을 결정해야 하는 상황이 있을 때 인덱스드 액세스 타입을 사용하여 객체의 속성을 동적으로 참조할 수 있다.
const PromotionList = [
{type:"product", name:"laptop"},
{type:"product", name:"keyboard"},
{type:"card", name:"SH"},
];
type ElementOf<T> = typeof T[number];
type PromotionItemType = ElementOf<PromotionList>;
맵드 타입은 다른 타입을 기반으로 한 타입을 선언할 때 사용하는 문법이며 인덱스 시그니처 문법의 keyof와 함께 사용되며 반복적인 타입 선언을 줄일 수 있다. 주로 객체의 속성들을 순회하며 새로운 속성을 만들거나 기존의 속성을 변환할 때 활용된다.
type Example = {
a: number;
b: string;
c: boolean
}
type Subset<T> = {
[K in keyof T]?:T[K];
}
const aExample:Subset<Example> = {a:100};
const bExample:Subset<Example> = {b:"string"};
const cExample:Subset<Example> = {c:true};
const dExpample:Subset<Example> = {d:true} // X, "개체 리터럴은 알려진 속성만 지정할 수 있으며 'Subset<Example>' 형식에 'd'이(가) 없습니다." 에러 발생
BottomSheet라는 컴포넌트에는 연락처와 장바구니가 있다고 하자. 여기에서 각각 resolver, isOpened 등의 상태를 관리하는 스토어가 필요한데 이 스토어의 타입을 선언해줘야한다.
이때 모든 키에 대해서 스토어를 만들어 줄 수 있지만 Mapped Types 문법을 사용하여 각 키에 해당하는 스토어를 선언할 수 있다.
const BottomSheetMap = {
CONTACT: "010-xxxx-xxxx",
CART: "taco"
}
// BottomId는 "CONTACT" | "CART";' 유니온 타입
type BottomId = keyof typeof BottomSheetMap;
// Mapped Types를 통한 타입 선언
type BottomSheetStore = {
[key in BottomId]: {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
};
/*
BottomSheetStore 타입은 다음과 같은 구조를 가짐:
{
'CONTACT': {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
'CART': {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
}
*/
또한 맵드 타입에서는 as 키워드를 사용하여 키를 재정의할 수 있다. 만약 위 코드에서 모든 키에 특정 문자열을 붙이는 식으로 공통된 처리를 하고싶으면 다음과 같이 사용하면 된다.
type BottomSheetStore = {
[key in BottomId as `${key}_BOTTOM_SHEET`]: {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
};
/*
type BottomSheetStore의 구조
{
CONTACT_BOTTOM_SHEET: {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
CART_BOTTOM_SHEET: {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
};
*/
자바스크립트의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법이다.
위에 작성한 각각의 BottomId의 키에 __BOTTOM_SHEET를 붙인 예시가 템플릿 리터럴을 활용한 것이다.
좀 더 간단한 예시는 다음과 같다.
type Status = "before" | "in progress" | "after";
type TestStatus = `Test-${Status}`
// "Test-before" | "Test-in progress" | "Test-after";
이처럼 템플릿 리터럴을 사용하여 새로운 문자열 리터럴 유니온 타입을 만들 수 있다.
제네릭은 C나 자바같은 정적언어에서 다양한 타입간의 재사용성을 높이기 위해 사용하는 문법이다. 타입스크립트도 정적언어로서 제네릭 문법을 지원한다.
제네릭은 일반화된 데이터 타입으로서 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워 둔 다음에 그 값을 사용할 때 외부에서 타입 변수자리에 타입을 지정하여 사용하는 방식으로 사용한다.
이럴경우 함수, 타입, 클래스 등 여러 타입에 대해 따로 정의하지 않아도 되기 때문에 재사용성이 크게 향상된다.
타입 변수는 일반적으로 <T>와 같이 꺾쇠괄호 내부에 정의되며 사용할 때 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣으면 된다.
타입 변수명으로는 보통 T(Type), K(Key), E(Element), V(Value)등 한 글자로 된 이름을 많이 사용한다.
type ExampleType<T> = T[]
const foodArray: ExampleType<string> = ["떡볶이", "순대", "튀김", "어묵"]
any는 타입 검사를 하지 않고 모든 타입이 허용되는 타입으로 취급되지만 generic은 any처럼 아무 타입이나 무분별하게 받는것이 아니라 배열 생성 시점에 원하는 타입으로 특정할 수 있다.
제네릭을 사용하면 배열 요소가 전부 동일한 타입이라고 보장할 수 있다.
제네릭은 일반화된 데이터 타입이다. 따라서 특정 타입에서만 존재하는 멤버를 참조하려고 하면 에러가 발생한다.
만약 특정 속성을 제네릭에서 참조하고 싶을 경우에는 제약을 걸어줌으로써 해당 속성을 사용할 수 있게 만들 수 있다.
다음의 코드에서 exampleFunc 함수는 제네릭 타입 T를 가지며, 이 타입은 TypeWithLength 인터페이스를 확장한 타입이어야 한다. TypeWithLength 인터페이스는 length 속성을 가져야 하는 제약을 가지고 있는 것이다.
interface TypeWithLength {
length: number;
};
function exampleFucn<T extends TypeWithLength>(arg:T):number{
return arg.length;
}
이러한 제약을 통해 exampleFunc 함수는 T가 TypeWithLength 인터페이스를 만족하는 경우에만 arg.length와 같은 특정 속성을 안전하게 사용할 수 있게 된다.
함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때 제네릭을 사용할 수 있다.
다음과 같이 T자리에 넣는 타입에 따라 적절하게 사용될 수 있다.
function ReadOnlyRepository<T>(
target: ObjectType<T> | EntitySchema<T> | string
): Repository<T> {
return getConnection('ro').getRepository(target);
}
아래의 코드는 우아한 타입스크립트 교재 110P에 있는 함수 타입의 호출 시그니처 예시 코드이다.
export type UserRequestHookType = <RequestData = void, ResponseData = void>(
baseURL?: string | Headers,
defaultHeader?: Headers
) => [RequestStatus, Requester<RequestData, ResponseData>];
해당 코드는 호출 시그니처의 제네릭 활용 예시로 함수를 선언한 뒤 타입을 UserRequestHookType으로 지정하여 함수를 호출할때 제네릭으로 구체적인 타입을 명시하면 된다.
예를 들어 userRequestFunction라는 함수를 선언하고 타입을 UserRequestHookType으로 지정하게 되면 userRequestFunction을 호출 시점에 <string, number>를 넣어서 <RequestData, ResponseData>에 들어갈 타입을 한정하는 것이다.
const defaultHeader:Headers = {header:"header"};
userRequestFunction<string, number>("baseURL",defaultHeader);
이런식으로 사용하면 RequestData에는 string 타입이 ResponseData에는 number 타입이 들어가게 된다.
제네릭 클래스는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스다.
쉬운 설명을 위해 책에 있는 예제가 아닌 다른 코드로 설명을 진행한다.
class Pair<T, U> {
private first: T;
private second: U;
constructor(first: T, second: U) {
this.first = first;
this.second = second;
}
getFirst(): T {
return this.first;
}
getSecond(): U {
return this.second;
}
setFirst(value: T): void {
this.first = value;
}
setSecond(value: U): void {
this.second = value;
}
}
// Pair 클래스의 인스턴스 생성
const numberAndStringPair = new Pair<number, string>(10, 'Hello');
// 값 확인
console.log(numberAndStringPair.getFirst()); // 출력: 10
console.log(numberAndStringPair.getSecond()); // 출력: Hello
// 새로운 값 설정
numberAndStringPair.setFirst(20);
numberAndStringPair.setSecond('World');
// 변경된 값 확인
console.log(numberAndStringPair.getFirst()); // 출력: 20
console.log(numberAndStringPair.getSecond()); // 출력: World
Pair 클래스의 T와 U는 클래스의 내부에서 사용되는 타입이다. 이러한 제네릭 타입 매개변수들은 클래스가 사용될 때 실제 타입으로 대체된다.
예를 들어, Pair<number, string>으로 인스턴스를 생성하면 <T>는 number로, <U>는 string으로 대체되어 클래스의 내부에서는 실제로 number와 string 타입을 다루는 것과 같은 효과를 얻는다.
제한된 제네릭은 타입 매개변수에 대한 제약 조건을 설정하는 기능을 말한다.
만약 A라는 타입을 B라는 타입으로 제약을 하기 위해서 A 타입의 매개변수는 extends키워드를 사용하여 B 타입을 상속해야한다.
예시 코드는 다음과 같다.
type Student = {
name: string;
age: number;
};
function printStudent<T extends Student>(obj: T, key: keyof T): void {
console.log(obj[key]);
}
printStudent의 타입 매개변수 T는 Student라는 타입으로 제약 조건이 설정되어 있다.
이처럼 타입 매개변수가 특정 타입에 묶여 있을 때 해당 키를 바운드 타입 매개변수라 부른다.
또한 상속된 Student는 T의 상한 한계라고 부른다.
추가로 타입스크립트는 구조적 타이핑의 특성을 지니고 있으므로 제한된 타입과 유사한 타입의 값을 넘겨받을 경우에는 에러가 발생하지 않는다.
하지만 유사하지 않은 타입을 값을 넘겨받을 경우 컴파일 에러가 발생한다.
// 제한된 제네릭으로 상속받은 Student 타입과 유사한 타입의 값을 넘겨받을 경우 컴파일 에러가 발생하지 않음
printStudent({ name: 'jay', age: 25 }, 'name'); // jay
printStudent({ name: 'mark', age: 20, class: 'A' }, 'name'); // mark
printStudent({ name: 'jhon', class: 'A', age: 27 }, 'name'); // jhon
// printStudent({name:"jay",class:"A"},"name") // 에러 발생
제네릭 타입은 여러 타입을 상속받을 수 있으며 타입 매개변수를 여러개 둘 수 있다.
하지만 <T extends string> 이런식으로 타입을 제약하면 제네릭의 유연성을 잃을 수 있다.
제네릭의 유연성을 잃지 않으며서 타입을 제약해야 할 때는 타입 매개변수에 유니온 타입 |을 상속해서 <T extends string | number>와 같이 선언하면 된다.
유니온 타입으로 T 가 여러 타입을 받게 할 수 있지만 타입 매개변수가 여러 개일 때는 처리할 수 없다. 이럴때는 매개변수를 추가하여 선언한다.
다음은 제네릭을 확장하고 매개변수를 추가한 케이스다.
// T는 string 또는 number 타입, U는 string 타입
function printType<T extends string | number, U extends string>(
a: T,
b: U
): void {
// T가 string인 경우
if (typeof a === 'string') {
console.log(`Type of 'a': string, Value of 'a': ${a.toUpperCase()}`);
}
// T가 number인 경우
else if (typeof a === 'number') {
console.log(`Type of 'a': number, Value of 'a': ${a.toFixed(2)}`);
}
console.log(`Type of 'b': ${typeof b}`);
}
제네릭의 장점은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이다.
그 중 API 응답 값의 타입을 지정할 때 현업에서 가장 많이 활용된다.
API 응답 값의 타입 지정시의 활용되는 예시는 다음과 같다.
export interface MobileApiResponse<Data>{
data:Data;
statusCode:string;
statusMessage?:string;
}
MobileApiResponse의 data라는 속성은 API의 응답 값에 따라서 달라질 것이다. 그러므로 제네릭을 사용하여 타입매개변수인 Data로 지정했다.
이후 interface MobileApiResponse 를 활용하는 코드는 다음과 같다.
// 어딘가에 선언된 타입 Price를 넘겨줌
const fetchPriceInfo = (): Promise<MobileApiResponse<Price>> => {
const priceUrl = 'price URL';
return request({
method: 'GET',
url: priceUrl,
});
};
// 어딘가에 선언된 타입 Order를 넘겨줌
const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
const orderUrl = 'order URL';
return request({
method: 'GET',
url: orderUrl,
});
};
이처럼 다양한 API 응답 값의 타입으로 MobileApiResponse를 재사용할 수 있다.
이런식으로 제네릭을 적재적소에 활용하면 가독성을 높이고 효율적인 코드 작성이 가능하지만 굳이 필요하지 않은 곳에서 사용하면 오히려 코드를 복잡하게 만들 수 있다.
제네릭을 굳이 사용하지 않아도 되는 타입
제네릭이 필요하지 않을 때 사용하면 코드 길이만 늘어나고 가독성을 해칠 수 있다.
type Gtype<T> = T;
type RequirementType = "USE"|"UN_USE"|"NON_SELECT";
interface Order {
gerRequirement():Gtype<RequirementType>
};
Gtype 이라는 이름은 목적의 의미를 담지도 않고 굳이 제네릭을 사용하지 않고 타입 매개변수를 그대로 선언하는 것과 같은 기능을 하고 있다.
따라서 위의 코드는 다음과 동일하다.
type RequirementType = "USE"|"UN_USE"|"NON_SELECT";
interface Order {
gerRequirement():RequirementType;
};
any 사용하기
type ReturnType<T = any> {
//...
}가독성을 고려하지 않은 사용
과도한 제네릭은 가독성을 해쳐 코드의 해석을 어렵게 한다. 복잡한 제네릭은 의미 단위로 분할해서 사용하는 것이 좋다.
// ReturnType<Record<OrderType,Partial<Record<CommonOrderStatus | CommonReturnStatus,Partial<Record<OrderRoleType,string[]>>>>>>
type CommonStatus = CommonOrderStatus | CommonReturnStatus;
type PartialOrderRole = Partial<Record<OrderRoleType,string[]>>;
type RecordCommonOrder = Record<CommonStatus, PartialOrderRole>;
type RecordOrder = Record<OrderType,Partial<RecordCommonOrder>>;
ReturnType<RecordOrder>