내부적으로 사용할 타입을 정해두지 않고 타입 변수를 사용해서 해당 위치를 비우고, 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식
function getSize(arr : number[]) : number {
return arr.length;
}
const arr1 = [1, 2, 3];
getSize(arr1); // 3
const arr2 = ["a", "b", "c"];
getSize(arr2); // error
이러한 상황에서, getSize의 타입이 불일치해 에러가 발생했다.
function getSize(arr : number[] | string[]) : number {
return arr.length;
}
이를 임시방편으로 해결하기 위해, 유니온 선언으로 확장했지만, boolean, Object등 더 많아지면 굉장히 지저분한 코드가 만들어진다.
function getSize<T>(arr: T[]): number {
return arr.length;
}
const arr1 = [1, 2, 3];
getSize<number>(arr1); // 3
이렇게 타입 변수로 와 같이 정의하고, 사용시엔 원하는 타입을 넣어주면 된다.
변수명으로 T(Type), E(Element), K(Key), V(Value) 등 한 글자로 된 이름을 많이 사용한다.
any
: 타입 검사를 하지 않고 모든 타입을 허용한다.
Generic
: 생성 시점에 원하는 타입으로 특정할 수 있다. 따라서, 요소가 전부 동일한 타입임을 보장할 수 있다.
function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string):
Repository<T> {
return getConnection(“ro”).getRepository(target);
}
위 예시는 데이터베이스의 읽기 전용 연결을 통해 특정 타입의 레포 객체를 가져오는 데 사용한다.
이처럼 T자리에 넣는 타입에 따라 ReadOnlyRepository가 적절하게 사용될 수 있다.
function sum(a: number, b: number): number {
return a + b
} // 이거를
type myFunction = (a: number, b: number) => number; // 이런식으로 만드는게
const thatsMine: myFunction = (a, b) => a + b; // 호출 시그니처
호출 시그니처(call signature)
: 타입스크립트의 함수 타입 문법으로, 함수의 매개변수와 반환 타입을 미리 선언하는 것
interface useSelectPaginationProps<T> {
categoryAtom: RecoilState<number>;
filterAtom: RecoilState<string[]>; sortAtom:
RecoilState<SortType>;
fetcherFunc: (props: CommonListRequest) = > Promise<DefaultResponse<ContentListRes
ponse<T>>>;
}
를 useSelectPaginationProps의 타입 별칭으로 한정했기에 제네릭 타입을 구체 타입으로 한정한다.
(만일 User Type으로 한정되었다면, ContentListResponse<T>
의 도 User type이기에 구체 타입으로 한정했다고 표현한다.)
export type UseRequesterHookType = <RequestData = void, ResponseData = void>(
baseURL?: string | Headers,
defaultHeader?: Headers
) => [RequestStatus, Requester<RequestData, ResponseData>];
이 코드의 경우에도 두개의 제네릭 타입을 구체 타입으로 한정했다.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
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;
}
// ...
}
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // error: T에는 .length가 없다.
return arg;
}
컴파일러는 모든 타입에서 .length
프로퍼티를 갖는지 증명할 수 없기에 에러를 띄운다.
따라서 특정 타입을 상속(extends
)을 통해 제약사항을 명시하는 방법을 활용한다.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 이제 .length 프로퍼티가 있는 것을 알기 때문에 더 이상 오류가 발생하지 않습니다.
return arg;
}
loggingIdentity(3); // 오류, number는 .length 프로퍼티가 없습니다.
loggingIdentity({length: 10, value: 3});
근데 이렇게 인터페이스 뿐만 아니라 기본 타입, 클래스도 사용할 수 있으며 유니온 타입을 상속해서 선언할 수도 있다.
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,
});
제네릭 타입은 여러 타입을 상속받을 수 있으며 타입 매개변수를 여러 개 둘 수도 있다.
<Key extends string>
이렇게 제약해버리면 제네릭의 유연성을 잃어버리기에, 유니온 타입을 상속해서 선언해버리자.
<Key extends string | number>
여러 타입을 받을 수 있게 되었지만, 타입 매개변수가 여러개일땐?
매개변수를 하나 더 추가하여 선언하면 된다.
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
);
};
APIResponse<Ok, Err = string>
두개의 타입 매개변수를 지정해줘야 하고, Err타입을 지정하지 않으면 자동으로 string타입이 지정된다는 의미이다.
돌고돌아 제네릭의 장점은, 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있다는 것이다.
현업에서 제네릭이 가장 많이 활용할때는, API 응답 값의 타입을 지정할 때이다.
export interface MobileApiResponse<Data> {
data: Data;
statusCode: string;
statusMessage?: string;
}
API응답 값에 따라 달라지는 data를 제네릭타입 Data로 선언하고
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>;
}
// 위 코드는 그냥 제네릭 안쓰고 아래처럼 작성하는게 낫다
type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
interface Order {
getRequirement(): RequirementType;
}
만약 내가 작성한 코드를 다른 개발자가 쉽게 이해하지 못하고있다면, 제네릭 오남용하고 있는건 아닌지 검토해보길 바란다.