# [TypeScript] 제네릭 개념과 사용하는 이유

YEONSUI 프로젝트를 진행하며 버튼 컴포넌트에 제네릭을 사용한 적 있다. (자세한 내용은 [지난 포스트](https://yeonsu.hashnode.dev/typescript-button)를 참고!)

그땐 문제를 해결하는 과정에 초점을 맞췄다. 오늘은 제네릭이라는 개념에 대해 정리해보려 한다.

## 🔵 제네릭(Generics)이란?

C#, Java와 같은 정적 언어에서 여러 가지 타입에서 동작하는 컴포넌트를 만들 때 재사용성을 높이는 용도로 활용된다. 함수를 정의하는 시점에 매개변수와 반환값의 타입을 선언해야 하는데, 그 시점에 타입을 선언하기 어려울 때 사용하면 된다.

즉, 제네릭은 타입을 함수의 파라미터처럼 사용하는 것을 의미한다.

```javascript
function getText(text) {
  return text
}
getText('hi') // 'hi'
getText(10) // 10
getText(true) // true
```

`getText` 함수는 `text`라는 파라미터에 값을 넘겨 받아 `text`를 반환한다.

```typescript
function getText<T>(text: T): T {
  return text
}
getText<string>('hi') // T = string
getText<number>(10) // T = number
getText<boolean>(true) // T = boolean
```

제네릭을 사용하면 함수를 호출할 때 꺽쇠 안에 타입을 넘겨줄 수 있다. 이때 타입명은 `T`가 아니여도 상관 없다.

## 🔵 제네릭 제약 조건

제네릭에 타입 변수를 제공하는 것 외에도 타입 힌트를 줄 수 있는 방법이 있다.

```typescript
function logText<T>(text: T): T {
  console.log(text.length) // ❌ Error: T doesn't have .length
  return text
}
```

위 함수는 `T`가 어떤 타입인지 정의하지 않았기 때문에 length의 유무를 알 수 없어 오류가 난다.

```typescript
type LengthWise = {
  length: number
}

function logText<T extends LengthWise>(text: T): T {
  console.log(text.length)
  return text
}

logText(10) // ❌ Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
logText({ length: 0, value: 'hi' })
```

위와 같이 타입에 제약조건을 추가하면 length를 갖고 있는 인자만 넘겨받을 수 있게 됩니다.

## 🔵 제네릭을 왜 사용해야 할까?

함수를 정의하는 시점에 타입을 선언하기 어려울 때 `any` 타입을 사용할 수도 있지 않을까?

```typescript
function getText(text: any): any {
  return text
}
```

`any` 타입을 사용한다 해서 함수가 동작하는 데 문제가 없다. 하지만 함수의 인자로 어떤 타입이 들어가고 반환되었는지 알 수 없다. 왜냐하면 `any` 타입은 타입 검사를 하지 않기 때문이다.

예를 들어, `ButtonStyle` 타입이 filled 또는 outlined이고, `ButtonVariant` 타입은 filled 일 때 primary, outlined 일 때 primary와 secondary라고 가정하자.

```typescript
type ButtonStyle = 'filled' | 'outlined'

type ButtonProps = {
  style: ButtonStyle,
  variant: any,
}

function getButtonStyle({ style, variant }: ButtonProps) {
  return `style: ${style}, variant: ${variant}`
}

getButtonStyle({ style: 'filled', variant: 'primary' })
  // style: filled, variant: primary
getButtonStyle({ style: 'filled', variant: 'secondary' })
  // style: filled, variant: secondary
getButtonStyle({ style: 'outlined', variant: 'secondary' })
  // style: outlined, variant: secondary
getButtonStyle({ style: 'outlined', variant: '' })
  // style: outlined, variant:
```

`variant` 타입을 any로 지정하니 `style` 타입에 대한 `variant` 타입을 검사하지 않기 때문에 타입 에러가 발생하지 않는다.

```typescript
type ButtonStyle = 'filled' | 'outlined'

type ButtonFilledVariant = 'primary'
type ButtonOutlinedVariant = 'primary' | 'secondary'

type ButtonVariant<T> = T extends 'filled'
  ? ButtonFilledVariant
  : T extends 'outlined'
  ? ButtonOutlinedVariant
  : never

type ButtonProps<T extends ButtonStyle> = {
  style: T,
  variant: ButtonVariant<T>,
}

function getButtonStyle<T extends ButtonStyle>({ style, variant }: ButtonProps<T>) {
  return `style: ${style}, variant: ${variant}`
}

getButtonStyle({ style: 'filled', variant: 'primary' })
  // style: filled, variant: primary
getButtonStyle({ style: 'filled', variant: 'secondary' })
  // ❌ Error: 'secondary' is not assignable to type 'ButtonFilledVariant'
getButtonStyle({ style: 'outlined', variant: 'secondary' })
  // style: outlined, variant: secondary
getButtonStyle({ style: 'outlined', variant: '' })
  // ❌ Error: '' is not assignable to type 'ButtonOutlinedVariant'
```

방금 살펴본 제네릭의 제약 조건을 사용해 `style` 타입으로 선언한 값을 인수로 지정하여 그에 대한 `variant` 타입을 지정하도록 한다. `any`와 달리 타입 검사를 하기 때문에 에러를 발견할 수 있다.

---

## 레퍼런스

[제네릭 | 타입스크립트 핸드북](https://joshua1988.github.io/ts/guide/generics.html)
