3장. 고급 타입
3.1 타입스크립트만의 독자적 타입 시스템
3.1.1 any 타입
- JS 에 존재하는 모든 값을 오류 없이 받을 수 있다. 즉, 이전의 JS 의 기본적 사용 방법
- 사용을 지양해야할 패턴, noImplicitAny 옵션으로 강제 가능
어쩔 수 없는 3가지 사례
- 개발 단계에서 임시로 갑 지정 필요 시
- 어떤 값을 받을지 또는 넘겨줄지 정할 수 없을 때
- 값을 예측할 수 없을 때 암묵적으로 사용
3.1.2 unknown 타입
-
unknown 타입에는 모든 타입 할당 가능, 반대로 any 를 제외한 다른 타입으로 선언된 변수에는 unknown 타입 할당 불가능
-
할당 시점에는 에러가 발생하지 않지만, 아무런 값이 할당 되지 않은채로 실행되면 컴파일 에러 발생
-
any 는 무조건 넘어가지만, unknown 은 타입을 강제하여 버그 가능성을 줄일 수 있다
-
unkown 을 사용하여 아직 어떤 값이 들어올지 모르는 메서드의 타입을 강제하는 코드
const jsonParserUnknown = (jsonString: string): unknown =>
JSON.parse(jsonString);
const myOtherAccount = jsonParserUnknown(`{ "name": "Samuel" }`);
console.log(myOtherAccount.name); // ERR
- 아래와 같이 타입을 정확히 지정해야만 컴파일 오류가 발생하지 않아 개발자의 실수를 줄일 수 있다
type jsonObj = {
name: string;
};
const jsonParserUnknown = (jsonString: string): jsonObj =>
JSON.parse(jsonString);
const myOtherAccount = jsonParserUnknown(`{ "name": "Samuel" }`);
console.log(myOtherAccount.name);
강제 형변환
- 보통 as unknown as Type 을 사용하여 변환한다
- 변환 대상의 서로 부모 자식 관계가 있으면, as unknown 생략이 가능 => 같은 타입이므로
- 부모 자식 관계가 아니면, as unknown 으로 변환하여 아무런 타입이나 할당이 가능한 상태로 만들어 다시 할당하는 방법을 사용한다
- 그런데, 이거 any 써도 무방하지 않나요? 암만 생각해도 그런듯
** [p.86] 깨진 유리창 이론에 대해서 어찌 생각하시나요? 실제로 사례를 본적이 있나요? ** [p.87] as unknown as Type 에 대해 왜 서로 싸우죠? ㅋㅋㅋ
const env = process.env as unknown as ProcessEnv;
const env = process.env as any as ProcessEnv;
** [p.87] 두 코드의 차이가 있을까요?
3.1.3 void 타입
- JS 에서 리턴이 없으면 undefined 가 자동으로 반환, 따라서 void 사용 필요!
- 변수에도 할당이 가능, 단 undefined 또는 null 만 지정 가능
3.1.4 never 타입
- 값을 반환할 수 없는 타입으로, 명시적으로 구분하여 사용이 필요
값을 반환할 수 없는 예
- 에러를 던지는 경우(throw 에 의한 에러)
- 무한히 함수가 실행되는 경우(무한 루프)
3.1.5 Array 타입
- Object.prototype.toString.call() 을 사용하면 Array 데이터 타입 확인 가능
- JS 와는 달리 TS 에서는 배열에 아무 데이터 타입만 넣는 것이 불가능 하며, 별도의 문법이 필요하므로 배울 필요가 있다
const array1: number[] = [1, 2, 3];
const array2: Array<number> = [1, 2, 3];
- 배열에 여러 타입을 관리하려면 유니온 타입을 사용
const array1: number[] | string[] = [1, 'str1'];
const array2: Array<number | string> = [2, 'str2'];
- 튜플은 이러한 TS 의 배열에 길이 제한까지 추가한 타입, 배열보다 강제성을 더 줄 수 있다
const tuple1: [number] = [1];
const tuple2: [number, string, boolean] = [1, 'str', true];
- 전개 연산자를 사용하여 튜플과 배열의 성질을 혼합하여 사용 가능
const httpStatusFromPaths: [number, string, ...string[]] = [
400,
'Bad Request',
'/users/:id',
'/users/:userId',
];
- 옵셔널 프로퍼티를 필요에 따라 사용 가능
const optionalTuple1: [number, number, number?] = [1, 2];
3.1.6 enum 타입
- 열거형 데이터 타입으로, 구조체를 만들 때 사용
- 각각의 열거 데이터에 0 부터 값을 삽입, 또는 명시 가능
- 문자열 상수를 만들어 타입 안정성, 가독성, 명확한 의미 전달 및 높은 응집력의 효과
- 단, TS 가 자동으로 추론한 값을 사용하는 것은 지양
- 단, 할당된 값을 넘어서는 범위의 경우 문제 발생의 여지가 있다
- enum 은 JS 로 변환시 IIFE 로 변환 되므로, 번들링의 사이즈가 증가하는 이슈 발생
- const enum 또는 as const assertion 을 사용해서 해결 가능
enum ProgrammingLang {
Typescript,
Javascript,
Java,
Cplusplus = 12,
}
console.log(ProgrammingLang.Typescript); // 0
console.log(ProgrammingLang.Cplusplus); // 12
console.log(ProgrammingLang[200]); // undefined
- const enum 을 사용하면 정의되지 않은 데이터에 대한 접근 및 의도하지 않은 값의 할당을 방지할 수 있다
const enum NUMBER {
ONE = 1,
TWO = 2,
}
const myNum: NUMBER = 100; // ERR
const enum STRING_NUMBER {
ONE = 'ONE',
TWO = 'TWO',
}
const myStringNum: STRING_NUMBER = 'THREE'; // ERR
** [p. 97] 이거 에러 나지 않나요?
3.2 타입 조합
3.2.1 교차 타입(Intersection)
- 여러가지 타입을 결합하여 하나의 단일 타입으로 만드는 것, & 을 사용하여 표기
- 결합한 타입을 모두 만족해야만 한다
type ProductItem = {
id: number;
name: string;
type: string;
price: number;
imgUrl: string;
quantity: number;
};
type ProductItemWithDiscount = ProductItem & { discountAmount: number };
3.2.2 유니온 타입(Union)
- 여러가지 타입을 결합하여 하나의 단일 타입으로 만드는 것, | 을 사용하여 표기
- 결합한 타입 중 하나가 될 수 있는 타입을 말한다
type ProductItem = {
id: number;
name: string;
type: string;
price: number;
imgUrl: string;
quantity: number;
};
type CardItem = {
id: number;
name: string;
type: string;
imgUrl: string;
};
type PromotionEventItem = ProductItem | CardItem;
const prontPromotionItem = (item: PromotionEventItem) => {
console.log(item.name);
console.log(item.quantity); // ERR, CardItem 에서는 quantity 를 보장하지 못함
};
3.2.3 인덱스 시그니쳐(Index Signatures)
- 속성 이름을 알 수 없지만 타입을 알고 있을 때 사용하는 문법
- 해당 문법을 통해 특정 타입 속성의 타입을 강제할 수 있다
- 인터페이스 내부에 [Key: K]: T 꼴로 타입을 명시한다
interface IndexSignatureEx {
[key: string]: number | boolean;
length: number;
isValid: boolean;
name: string; // ERR, number 혹은 boolean 만 가능
}
** [p.101] 이런 부분 표현이 조금 재미 있는 것 같습니다. 다른 타입의 특정 속성이 가지는 타입?
3.2.4 인덱스드 엑세스 타입(Indexed Access Types)
- 다른 타입의 특정 속성이 가지는 타입을 알아내거나, 해당 타입을 받아오기 위해 사용
type Example = {
a: number;
b: string;
c: boolean;
};
type indexedAccess = Example['a']; // number
type indexedAccess2 = Example['a' | 'b']; // number | string
type indexedAccess3 = Example[keyof Example]; // number | string | boolean
const indexedAccessVar: indexedAccess = 1;
const indexedAccessVar2: indexedAccess2 = 's';
const indexedAccessVar3: indexedAccess3 = false;
- 특정 배열의 요소 타입을 받기 위해 아래와 같은 코드로 구현이 가능하다
const promotionList = [
{ type: 'product', name: 'chicken' },
{ type: 'product', name: 'pizza' },
{ type: 'card', name: 'cheer-up' },
];
// 책의 에러 코드, 해당 코드는 배열 타입 선언에만 사용 가능
// type ElementOf<T> = typeof T[number];
// 입력받은 제너릭이 특정 타입으로 구성 된 배열인지를 확인하고
// 해당 타입의 배열이 맞다면 배열의 요소를 T[number] 로 인덱싱 하여 요소의 타입을 반환
// 아닐 경우 never 타입으로 반환
type ElementOf<T> = T extends any[] ? T[number] : never;
// promotionList 의 배열 타입을 전달 하여 promotionList 의 요소 타입을 반환 받기
type PromotionElementType = ElementOf<typeof promotionList>; // { type: string; name: string; } | { type: string; name: string; }
const PromotionElementTypeObj: PromotionElementType = {
type: 'type',
name: 'name',
};
// 객체일 경우 예시 코드
const promotionObj = {
promotionListType: {
type: 'type',
name: 'name',
},
};
// type ElementOf<T> = typeof T[number];
type PropertyOf<T> = T extends object ? T : never;
// ElementOf 타입 테스트
type PromotionProertyType = PropertyOf<typeof promotionObj>; // { type: string; name: string; } | { type: string; name: string; }
const PromotionPropertyTypeObj: PromotionProertyType = {
promotionListType: {
type: 'type',
name: 'name',
},
};
3.2.5 맵드 타입(Mapped Types)
- 다른 타입을 기반으로 다른 타입을 선언할 때 사용하는 문법
- 인덱스 시그니처 문법을 사용하면 반복적인 타입 선언을 효과적으로 줄일 수 있다
type ExampleMapped = {
a: number;
b: string;
c: boolean;
};
type Subset<T> = {
[K in keyof T]?: T[K];
};
const aExampleMapped: Subset<ExampleMapped> = { a: 3 };
const bExampleMapped: Subset<ExampleMapped> = { b: 'hello' };
const cExampleMapped: Subset<ExampleMapped> = { c: true };
- 맵드 타입에서 매핑 시, readonly 또는 ? 수식어 적용이 가능하며, - 를 추가하면 수식어를 제거한 타입 선언이 가능
-readonly [P in keyof T]: T[P];
로 쓰면 readonly 제거[P in keyof T]-?: T[P];
로 쓰면 ? 제거
type ReadOnlyEx = {
readonly a: number;
readonly b: string;
};
let objByReadOnlyEx: ReadOnlyEx = {
a: 1,
b: '2',
};
// objByReadOnlyEx.a = 3; // ERR
console.log(objByReadOnlyEx); // { a: 1, b: '2' };
type CreateMutable<T> = {
// readonly 속성을 제거하여 값을 변화해도 문제 X
-readonly [P in keyof T]: T[P];
};
type ChangedMutableTypeFromReadOnlyEx = CreateMutable<ReadOnlyEx>;
let objByChangedMutableTypeFromReadOnlyEx = {
a: 1,
b: '2',
};
objByChangedMutableTypeFromReadOnlyEx.a = 3;
objByChangedMutableTypeFromReadOnlyEx.b = '4';
console.log(objByChangedMutableTypeFromReadOnlyEx); // { a: 3, b: '4' };
type OptionalEx = {
a?: number;
b?: string;
c: boolean;
};
type Concrete<T> = {
// ?(optaional) 속성을 제거하여, 해당 속성이 반드시 존재하도록 수정
[P in keyof T]-?: T[P];
};
type ChandedConcreteTypeFromOptionalEx = Concrete<OptionalEx>;
let objByChandedConcreteTypeFromOptionalEx: ChandedConcreteTypeFromOptionalEx =
{
a: 1,
b: '2',
c: true,
};
console.log(objByChandedConcreteTypeFromOptionalEx); // { a: 1, b: '2', c: true };
- 배민 예시 코드
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 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 BottomSheetStoreByMapped = {
[index in BOTTOM_SHEET_ID]: {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
};
- as 키워드를 사용하여 키를 재지정하는 코드
type ChagnedBottomSheetStore = {
[index in BOTTOM_SHEET_ID as `${index}_BOTTOM_SHEET`]: {
resolver?: (payload: any) => void;
args?: any;
isOpened: boolean;
};
};
** [p.105] 아 이런건 진짜 좀.... ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
3.2.6 템플릿 리터럴 타입(Template Literal Types)
- JS 의 템플릿 리터럴 문자열을 사용하여 기존 타입에 별도의 문자열을 추가한 타입을 선언하는 문법
type Stage =
| 'init'
| 'select-image'
| 'edit-image'
| 'decorate-card'
| 'capture-image';
type StageName = `${Stage}-stage`;
// type StageName = "init-stage" | "select-image-stage" | "edit-image-stage" | "decorate-card-stage" | "capture-image-stage"
3.2.7 제네릭(Generic)
- 타입 간의 재사용성을 높이기 위해 사용되는 문법으로 C 나 JAVA 에서 가져온 것
- 사용할 타입을 미리 정해두지 않고 실제로 값을 사용할 때 타입을 지정하여 사용하는 방식이다
- 주로 한글자로 축약한 T(Type), E(Element), K(Key), V(Value) 를 사용한다
- 타입 추론이 가능한 경우에는
<>
내부에 타입을 명시하지 않아도 컴파일러가 추론하여 대입한다
function exampleFunc<T>(arg: T): T[] {
return new Array(3).fill(arg);
}
console.log(exampleFunc('hello')); // [ 'hello', 'hello', 'hello' ]
console.log(3); // [ 3, 3, 3 ]
- 다만 제네릭은 타입이 할당 되지 않은 타입이므로 특정 타입에만 존재하는 값을 참조하려고 하면 에러가 발생
function exampleFunc2<T>(arg: T): number {
return arg.length; // ERR
}
- 대신 제네릭에 extends 키워드를 통해 제약을 걸어주면 해당 속성을 사용할 수 있다
interface TypeWithLength {
length: number;
}
function exampleFunc3<T extends TypeWithLength>(arg: T): number {
return arg.length;
}
- 단, TSX 파일 내부에서는 제네릭의 꺽쇠(
<>
)와 JSX 의 꺽쇠를 혼동하기 때문에 extends 키워드를 사용하거나, 함수 선언을 function 으로 한다
// <T> 를 태그로 인식하여 에러 발생
const arrowExampleFunc = <T>(arg: T) => {
return new Array(3).fill(arg);
};
// No ERR
const arrowExampleFunc2 = <T extends {}>(arg: T) => {
return new Array(3).fill(arg);
};
// No ERR
function normalExampleFunc<T>(arg: T) {
return new Array(3).fill(arg);
}
3.3 제네릭 사용법
3.3.1 함수의 제네릭
- 함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때 사용
import { ObjectType, EntitySchema, Repository, getConnection } from 'typeorm';
function ReadOnlyRepository<T>(
target: ObjectType<T> | EntitySchema<T> | string
): Repository<T extends any ? any : never> {
return getConnection('ro').getRepository(target);
}
** [p.109] 안되는 코드 왜케 많죠?
3.3.2 호출 시그니처의 제네릭
- 함수의 매개변수와 반환 타입을 미리 선언하는 것
- 제네릭 타입을 어디에 위치 시킬지에 따라 제네릭을 언제 구체 타입으로 한정할지 결정 가능
interface useSelectPaginationProps<T> {
categoryAtom: RecoilState<number>;
filterAtom: RecoilState<string[]>;
sortAtom: RecoilState<SortType>;
// 훅 사용시에 제네릭을 받아서, 해당 런타임에 타입 결정
fetcherFunc: (
props: CommonListRequest
) => Promise<DefaultResponse<ContentListResponse<T>>>;
}
- 배민에서 사용하는 사용자 Hook 타입과 실제 코드
// 통신 결과 타입 정의
type RequestStatus = 'idle' | 'loading' | 'success' | 'error';
// 실제로 요청을 보내는 함수 타입 선언
type Requester<RequestData, ResponseData> = {
sendRequest: (requestData: RequestData) => Promise<ResponseData>;
status: RequestStatus;
};
// 훅 타입을 정의하여 useState 와 같이 [] 로 status 와 리퀘스트 실행 함수를 받을 수 있도록 설정
export type UseRequesterHookType = <RequestData = void, ResponseData = void>(
baseURL?: string | Headers,
defaultHeader?: Headers
) => [RequestStatus, Requester<RequestData, ResponseData>];
// 실제 훅 타입을 적용하여 훅 선언
const useRequesterHook: UseRequesterHookType = <
RequestData = void,
ResponseData = void
>(
baseURL?: string | Headers,
defaultHeader?: Headers
) => {
const [status, setStatus] = useState<RequestStatus>('idle');
const sendRequest = async (
requestData: RequestData
): Promise<ResponseData> => {
setStatus('loading');
try {
// 여기에서 실제 요청을 보냅니다.
const responseData = await fetch(baseURL as string, {
method: 'POST',
headers: defaultHeader,
body: JSON.stringify(requestData),
});
setStatus('success');
return responseData.json();
} catch (error) {
setStatus('error');
throw new Error('Request failed');
}
};
return [status, { sendRequest, status }];
};
// 사용 예시
const [requestStatus, requester] = useRequesterHook<
{ id: number },
{ name: string }
>('http://api.example.com', {
'Content-Type': 'application/json',
});
// 요청 보내기
requester
.sendRequest({ id: 123 })
.then((response) => console.log('Response:', response))
.catch((error) => console.error('Error:', error));
// 상태 확인
console.log('Request status:', requestStatus);
3.3.3 제네릭 클래스
- 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스
- 클래스 이름 뒤에 타입 매개변수인
<T>
를 선언하여 사용하며, DB 의 데이터 타입에 따라 해당 타입을 전달하여 사용이 가능
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에서 가져 옴*/
});
}
}
3.3.4 제한된 제네릭
- 매개변수에 대한 제약 조건을 설정하는 기능
- 타입 매개변수에 extends 키워드를 사용하여 특정 타입으로 제약 조건을 설정 가능
- 특정 조건인지를 extends 키워드로 확인하여 3항 연산자로 동적 타입 설정도 가능
type ErrorRecord<Key extends string> = Exclude<Key, ErrorCodeType> extends never
? Partial<Record<Key, boolean>>
: never;
- 위의 코드에서 Key 는 특정 타입으로 묶여있으므로 바운드 타입 매개변수라 부르고, 특정 타입인 string 을 상한 한계라고 한다
function useSelectPagination<
T extends CardListContent | CommonProductResponse
>({
filterAtom,
sortAtom,
fetcherFunc,
}: useSelectPaginationProps<T>): {
intersectionRef: RefObject<HTMLDivElement>;
data: T[];
categoryId: number;
isLoading: boolean;
isEmpty: boolean;
} {
// ...
}
// 사용하는 쪽 코드
const { intersectionRef, data, isLoading, isEmpty } =
useSelectPagination<CardListContent>({
categoryAtom: replyCardCategoryIdAtom,
filterAtom: replyCardFilterAtom,
sortAtom: replyCardSortAtom,
fetcherFunc: fetchReplyCardListByThemeGroup,
});
- 제네릭을 받아 동적으로 데이터를 리턴하는 형태의 커스텀 훅에 적용이 가능하다
3.3.5 확장된 제네릭
- 제레닉 타입은 여러 타입을 상속 가능하며 매개 변수를 여러개 사용이 가능하다
// 타입이 string 으로 제약되어 제네릭의 유연성이 떨어짐
type ConcreteType<Key extends string> = {
data: Key;
};
// 유니온 타입을 상속하여 제네릭의 유연성은 지키면서 타입을 제약하는 방법
type FlexibleType<Key extends string | number> = {
data: Key;
};
- 유니온을 이용하면 하나의 제네릭이 여러 타입을 받게 할 수 있지만, 타입 매개변수가 여러개일 경우에는 제네릭을 하나 더 추가하여 선언
3.3.6 제네릭 예시
- 제네릭의 장점은 다양한 타입을 받을 수 있어, 코드를 효율적으로 재사용할 수 있다는 점이다
- 현업에서는 API 응답 값의 타입 지정에 주로 사용된다
export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
const priceUrl = 'http://api.price.com';
return request({
method: 'GET',
url: priceUrl,
});
};
export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
const orderUrl = 'http://api.order.com';
return request({
method: 'GET',
url: orderUrl,
});
};
- 각기 다른 요청에 다른 응답 데이터를 제너릭을 이용하여 하나의 타입으로 처리
제네릭을 굳이 사용하지 않아도 되는 타입
- 제네릭을 무분별하게 사용하면 코드 길이만 늘어나고 가독성을 해칠 수 있다
// 재사용이 없을 코드에 적용한 경우
type GType<T> = T;
type RequirementType = 'USE' | 'UN_USE' | 'NON_SELECT';
interface Order {
getRequirement(): GType<RequirementType>;
}
// 제네릭을 제거한 버전
type RequirementType = 'USE' | 'UN_USE' | 'NON_SELECT';
interface Order {
getRequirement(): RequirementType;
}
any 사용하기
- 쓰지 말자
가독성을 고려하지 않은 사용
- 과하게 중첩되어 사용될 경우 가독성을 해치게 되므로, 필요에 따라 의미 단위로 분할하여 적용하는 편이 좋다
// 과한 중첩이 사용 된 경우
type 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>>;
type ReturnType2<RecordOrder>;