참고자료
- O’reilly 타입스크립트 프로그래밍
- ChatGPT 4.0
Generic
1. TypeScript Generics 이해하기
TypeScript의 Generics은 함수나 클래스의 인스턴스 생성 시점에 그 타입을 선언하는 방식입니다. 이를 통해 재사용 가능한 코드를 만들 수 있으며, 타입 안정성을 유지하면서 유연성을 높일 수 있습니다.
Generics는 타입 변수로, 보통 T라는 글자를 많이 사용합니다. 이 변수는 아무 타입이나 대입될 수 있습니다.
function identity<T>(arg: T): T {
return arg;
}2. Generics 예제
Generics는 다양한 방법으로 사용될 수 있습니다. 위의 예제는 가장 기본적인 형태이며,
이를 다양한 방식으로 확장할 수 있습니다.
2.1 함수와 Generics
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString");
console.log(output); // "myString"2.2 배열과 Generics
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length
return arg;
}참고: Array<T> = T[]
3. Generics에 제약조건 설정 : extends
Generics에 제약조건을 추가하는 것도 가능합니다. 이는 extends 키워드를 사용하여 수행됩니다.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;4. Generic Types and Classes
4.1 Generic Types
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;4.2 Generic Classes
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; };
5. 언제 Generic Parameter가 한정되는가?
제네릭 함수와 제네릭 별칭의 차이
- 제네릭 별칭(Generic Type Alias): 해당 타입 별칭(Type Alias)을 사용할 때 parameter에 타입이 한정된다.
- 제네릭 함수(Generic Function) : 함수가 호출될 때 받은 parameter에 따라 타입이 한정된다.
차이가 뭐지? 말장난 같은데?라는 생각이 든다면 예시를 보자.
// Generic Type Alias
type SliceFruit<Fruit> = (fruit: Fruit): Fruit[];
const sliceBanana: SliceFruit<Banana> = (banana) => {
// ...
}
const Component: React.FC<ComponentProps> = () => {}위에서 Fruit 는 SliceFruit 범위로 한정되어있다.
그리고 해당 타입을 사용할 때 Fruit을 명시적으로 구체화해야한다.
SliceFruit<Banana> , React.FC<CompoenntProps> 부분이 타입을 사용할 때 이다.
이 때 인수를 꺽쇠 안에 넣어 제네릭 파라미터의 타입을 구체화하고 있다.
// Generic Function Type
type SliceFruit = <Fruit>(fruit: Fruit): Fruit[];
const sliceBanana: SliceFruit = (banana) => {};여기서는 <Fruit>을 호출 시그니쳐의 일부로 선언하였기 때문에 함수가 호출되는 시점에서 Fruit의 타입이 추론될 것이다.
즉 Fruit의 범위는 SliceFruit이라는 type이 아니라 함수 호출 시그니쳐 부분 (fruit: Fruit): Fruit[] 이라고 할 수 있다.
정리 - 제네릭 타입이 구체화되는 시점 비교
- Generic Function Type: 함수로 전달된 인수를 통해서 함수가 호출될 때 제네릭 타입이 구체화된다.
- Generic Type Alias : 해당 alias assertion등으로 사용할 때 꺽쇠 안에 타입 인수를 넘겨 제네릭 타입이 구체화된다.
개발에 적용한 부분
Mui에서 제공하는 Date/TimePicker 들은 모두 LocalizationProvider로 감싸주어 사용해야하는데, 여기에 사용하는 date 라이브러리의 dateAdaptor를 prop으로 넘겨준다.
이 때 DatePickerProps 등 내부 prop에서는 TDate라는 제네릭 파라미터를 사용하는데,
TDate의 타입은 분명 해당 컴포넌트가 LocalizationProvider 내에서 사용될 때 특정된다.
// mui DateCalendar.d.ts
export interface DateCalendarProps<TDate>extends ExportedDateCalendarProps<TDate>, ExportedUseViewsOptions<DateView>,
SlotsAndSlotProps<DateCalendarSlotsComponent<TDate>,
DateCalendarSlotsComponentsProps<TDate>> {
/**
* The selected value.
* Used when the component is controlled.
*/
value?: TDate | null;
/**
* The default selected value.
* Used when the component is not controlled.
*/
defaultValue?: TDate | null;Generic Alias만 사용해본 나로서는, 어떻게 generic parameter가 함수가 사용되면서 특정되게 할 수 있는지 알 수 없었다. 항상 타입을 선언할 때 generic parameter를 특정해주어야했기 때문이다.
하지만 내부 mui 코드를 보면, DatePicker 등 Date 컴포넌트는 다음과 Generic Function Type을 사용하고 있다.
type DateCalendarComponent = (<TDate>(props: DateCalendarProps<TDate> & React.RefAttributes<HTMLDivElement>) => JSX.Element) & {
propTypes?: any;
};
type DateTimeFieldComponent = (<TDate>(props: DateTimeFieldProps<TDate> & React.RefAttributes<HTMLDivElement>) => JSX.Element) & {
propTypes?: any;
};이와 같이 구현하려고 했던 DateRangePicker 컴포넌트도 Generic Function Type을 사용하여 호출될 때 generic parameter가 구체화될 수 있게 되었다.
type MyDateRangePickerComponent = <TDate>(
props: MyDateRangePickerProps<TDate> & React.RefAttributes<HTMLDivElement>
) => JSX.Element;5. Generics를 사용하는 이유
Generics를 사용하면 여러 종류의 타입을 한 가지 방법으로 처리할 수 있다.
⇒ 다양한 타입을 지원하는 함수나 클래스를 만드는 데 유용.
⇒ 코드의 재사용성을 높이고 타입 안전성을 보장하여 더욱 견고한 코드를 작성하는 데 도움이 됩니다.
그냥.. 개발하다보면 필요할 때가 온다.
이때다! 제네릭 너가 필요하구나! 드디어 써주지…
사용 예시 : Elice Kdt Survey
(원래 독립적인 패키지로 개발하려했으나 시간관계상 kdt 프로젝트에 붙은 survey 프로젝트)
대략적 프로젝트 설명 ; 설문을 위한 여러 문항 타입을 정의하고, 서버에서 받은 question.type에 따라 다른 문항타입을 렌더한다.
export type QuestionDataType =
| TextInputQuestion
| UrlInputQuestion
| EmailInputQuestion
| DateInputQuestion
| PhoneNumberInputQuestion
| AddressInputQuestion
| SingleSelectQuestion
| MultipleSelectQuestion;이렇게 8개의 문항 타입이 있습니다.
그럼 8개의 문항 컴포넌트마다 Props type을 선언해야할텐데요,
예를 들면, DateInput 은 minDateTime 같은 옵션을 받을 수 있겠죠.
option은 달라지겟지만, 그 외의 기본적인 문항 컴포넌트의 prop구조는 같기 때문에 다른점만 갈아끼우는 타입은 없을까? 생각했습니다.
export interface QuestionProps<T extends QuestionDataType> {
question: T;
control: Control;
}그래서 이렇게 QuestionProps 하나만을 선언해서 사용하게 되었습니다.
문항 컴포넌트는 항상 control prop과 해당 문항 타입에 따라 다른 options을 받는 구조입니다.
export interface DateInputQuestion extends BaseQuestion {
options?: QuestionOptions<DateInputOptions>;
}- BaseQuestion
이렇게 위에서 선언한 XXQuestion type은 BaeQuestion을 Extends합니다.
여기에 공통적인 부분들이 들어가있고, 유일한 차이점인 options 필드만 제네릭 파라미터에 의해 달라지게 됩니다.
export type QuestionOptions<T extends OptionsType> = T;참고로 QuestionOptions<T>에 들어가는 T 파라미터 또한 OptionsType으로 한정지었습니다.

이렇게 한정된 type에 포함되지 않는 경우 type error가 발생합니다.
결국 이렇게 컴포넌트 prop을 사용하고, 문항마다 다른 option 프롭을 사용할 수 있게됨!
// DateInput 컴포넌트 예시
const DateInput: React.FC<QuestionProps<DateInputQuestion>> = ({
question,
control,
}) => {
const { options } = question;
// 해당 option을 사용해서 문항에서 필요한 validation 구현
<DatePickerComponent
value={value}
onChange={handleDateChange}
inputRef={ref}
// MUI DatePicker prop
**mask={
options?.getTime
? DATE_TIME_PICKER_INPUT_MASK
: DATE_TIME_INPUT_MASK
}
inputFormat={
options?.getTime
? DATE_TIME_PICKER_INPUT_FORMAT
: DATE_PICKER_INPUT_FORMAT**
}
//...
/>Utility Type
쉬어가는 타임
1. Partial<T>
사용 대상 : 객체 타입
Partial<T> 유틸리티는 타입 T의 모든 속성을 선택적(Optional)으로 만듭니다. 이는 객체에서 일부 속성만 설정하려는 경우 유용합니다.
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}2. Readonly<T>
사용 대상: 배열, 객체, 튜플 타입
**Readonly<T>**는 타입 T의 모든 속성을 읽기 전용으로 만듭니다.
이를 사용하면 객체의 속성을 변경하는 것을 방지할 수 있습니다.
interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Delete inactive users",
};
todo.title = "Hello"; // Error: title is read-only3. Record<K,T>
**Record<K,T>**는 K의 모든 속성을 T로 매핑하여 새로운 타입을 생성합니다.
interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
const nav: Record<Page, PageInfo> = {
home: { title: "Home" },
about: { title: "About" },
contact: { title: "Contact" },
};4. Pick<T, K>
사용 대상: 객체 타입
**Pick<T, K>**는 타입 T에서 K 속성을 선택하여 새로운 타입을 만듭니다.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">; // 여러개 pick할 때 union 사용
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};5. Omit<T, K>
사용 대상: 객체 타입
**Omit<T, K>**는 타입 T에서 K 속성을 제외한 새로운 타입을 만듭니다.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};6. Exclude<T, U>
사용 대상: 유니온 타입
**Exclude<T, U>**는 T union 타입에서 특정 타입을 제외합니다.
(Omit의 union버전)
type T = number | string;
type U = Exclude<T, number>; // Result: string7. Extract<T, U>
사용 대상: 유니온 타입
**Extract<T, U>**는 T에서 U에 할당 가능한 타입을 추출합니다.
type T = number | string;
type U = Extract<T, number>; // Result: number8. NonNullable<T>
사용대상 : Nullable 타입
**NonNullable<T>**는 T에서 null과 undefined를 제외한 타입을 생성합니다.
type T = string | number | null | undefined;
type U = NonNullable<T>; // Result: string | number9. ReturnType<T>
사용 대상: 함수 타입
**ReturnType<T>**는 함수 T의 반환 타입을 얻습니다.
⇒ 특정 함수타입에서 사용하는 리턴 타입을 Import할 필요가 없어서 편리할듯
TS Playground - An online editor for exploring TypeScript and JavaScript (opens in a new tab)
type T = () => string;
type U = ReturnType<T>; // Result: string10. InstanceType<T>
사용 대상: 클래스 생성자 타입
**InstanceType<T>**는 생성자 함수 T의 인스턴스 타입을 얻습니다.
class C {
x = 0;
y = 0;
}
type T = InstanceType<typeof C>; // Result: C
이러한 유틸리티 타입들은 복잡한 타입 작업을 수행할 때 매우 유용하며, TypeScript에서 타입 안전성을 확보하는 데 큰 도움이 됩니다.
11. ContructorParameters<T>
사용대상 :클래스 생성자 타입
생성자 함수의 매개변수 타입들을 추출하는 데 사용됩니다.
class Person {
constructor(name: string, age: number) {
// 생성자 코드
}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>;
// PersonConstructorParams의 타입은 [string, number]
12. Parameters<T>
사용 대상 : 함수 타입
함수 매개변수 타입들로 구성된 튜플 타입을 얻습니다.
type MyFunction = (name: string, age: number) => void;
type MyFunctionParams = Parameters<MyFunction>;
// MyFunctionParams의 타입은 [string, number]13. Required<T>
사용 대상: 객체 타입
객체의 모든 프로퍼티를 함수로 만듭니다.
export interface PhoneNumberOptions {
countryOptionList?: CountryCode[];
countryNameLanguage?: string;
}
type RequiredPhoneNumberOptions = Required<PhoneNumberOptions>
// PhoneNumberOptions의 타입은 { countryOptionList: CountryCode[], agcountryNameLanguagee: string }14. ReadonlyArray<T>
사용 대상: 모든 타입
주어진 타입으로 불변 배열을 생성한다.
type MyReadonlyArray = ReadonlyArray<number>;
const arr: MyReadonlyArray = [1, 2, 3, 4];
arr.push(5); // Error: push 메서드는 읽기 전용 배열에 존재하지 않습니다.
arr[0] = 10; // Error: 배열 요소는 읽기 전용입니다.