# [TypeScript] 제네릭으로 Button 컴포넌트 타입 안정성 높이기

YEONSUI 버전 2의 네 번째 작업은 버튼 컴포넌트를 정의하는 것이다.

피그마로 버튼 컴포넌트를 미리 디자인하였다. 스타일은 다음과 같다.

| Style | Variant |
| --- | --- |
| filled | primary |
| outlined | primary, secondary, tertiary |
| ghost | primary, secondary |
| icon | primary, secondary, filled outlined |

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720778776309/11421828-b2bf-4eca-b1b1-3a6b5fc663bb.png align="center")

각 Style마다 다른 Variant 옵션을 갖고 있다.

이제 버튼 컴포넌트를 정의하는데 필요한 props 타입을 정의하고 사용해보자.

```abap
type ButtonStyle = 'filled' | 'outlined' | 'ghost' | 'icon'
type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'filled' | 'outlined'

export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
  styleType: ButtonStyle
  variant?: ButtonVariant
  ...
}
```

```typescript
export const ButtonExample = () => (
  <>
    <Button styleType="filled" variant="primary">Type ⭕️</Button>
    <Button styleType="filled" variant="secondary">Type ❌</Button>
    <Button styleType="outlined" variant="filled">Type ❌</Button>
  </>
)
```

여기서 한 가지 문제를 발견하였다. `styleType` 값과 매치하지 않은 `variant` 값이 함께 사용되어도 에러가 발생하지 않는 것이다. *filled* style은 *secondary* variant를 갖고 있지 않고, *outlined* style은 *filled* variant를 갖고 있지 않다.

이것을 해결하기 위해 `styleType` 값에 따라 `variant` 타입이 바뀌도록 수정해야 한다.

타입스크립트 개념을 공부하면서 도대체 언제 이걸 쓰지? 의문이 들었던 이것을 드디어 쓸 날이 왔다. 바로, **제네릭**이다.

> ### 제네릭(Generics)이란?
> 
> 타입스크립트에서 함수, 클래스, 인터페이스 등을 작성할 때 타입을 미리 지정하지 않고, 사용할 때 지정할 수 있게 해주는 기능이다. 제네릭을 사용하면 다양한 타입에서 재사용할 수 있는 컴포넌트나 함수 등을 작성할 수 있어 코드의 유연성과 타입 안전성을 높일 수 있다.
> 
> 주로 &lt;&gt; 안에 파라미터를 정의하여 사용한다.

### 1\. 각 Style에 대한 Variant 타입 정의하기

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

type ButtonFilledVariant = 'primary'
type ButtonOutlinedVariant = 'primary' | 'secondary' | 'tertiary'
type ButtonGhostVariant = 'primary' | 'secondary'
type ButtonIconVariant = 'primary' | 'secondary' | 'filled' | 'outlined'
```

우선, 버튼의 Style과 그에 대한 각 Variant 타입을 정의한다.

### 2\. 제네릭 타입 매핑

```typescript
type ButtonVariant<T> = T extends 'filled'
  ? ButtonFilledVariant
  : T extends 'outlined'
  ? ButtonOutlinedVariant
  : T extends 'ghost'
  ? ButtonGhostVariant
  : T extends 'icon'
  ? ButtonIconVariant
  : never
```

제네릭을 사용해 Style에 따라 변형을 매핑하는 타입을 정의한다.

조건부 타입이 `T extends U ? X : Y` 라면 \*"T가 U에 할당 가능하면 X 타입을, 그렇지 않다면 Y 타입 반환"\*을 뜻한다.

`T`가 filled라면 `ButtonFilledVariant`, outlined라면 `ButtonOutlinedVariant`, ..., 아무것도 아니라면 `never` 타입이 `ButtonVariant` 타입이 된다.

### 3\. ButtonProps 타입 정의

```typescript
export interface ButtonProps<Style extends ButtonStyle> extends HTMLAttributes<HTMLButtonElement> {
  styleType: Style
  variant?: ButtonVariant<Style>
}
```

제네릭으로 `ButtonStyle` 타입을 지닌 `Style` 파라미터를 받는다. `styleType`은 `Style` 타입을 가지고, 이것은 `ButtonStyle` 중 하나이다.

`variant`는 `ButtonVariant` 타입을 통해 `Style`에 따라 적절한 변형 타입을 반환한다.

### 4\. Button 컴포넌트 적용

```typescript
export const Button = <Style extends ButtonStyle>({
  styleType,
  variant = 'primary',
}: ButtonProps<Style>) => {
  ...
}
```

Button 컴포넌트는 `styleType`을 필수로 받고, `variant`는 `styleType`에 따라 변형된 타입 내에서 자유롭게 받는다.

처음에 다뤘던 코드를 다시 살펴보면 내가 원하던 타입 에러가 발생했다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720781096383/d86f139c-785b-44eb-837d-42dfac2209f8.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720781082180/d6d5f323-0602-4995-af0a-137ed2f79059.png align="center")

그런데 한 가지 문제가 또 있다. 아래 코드에서 `variant`에 primary 타입이 존재하지 않다는 타입 에러가 발생한 것이다.

```typescript
export const Button = <Style extends ButtonStyle>({
  styleType,
  variant = 'primary',
}: ButtonProps<Style>) => {
  ...
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1720781191189/50840fe4-5768-47e2-8083-419256c7f83d.png align="center")

이것은 제네릭에 아무런 값이 없기 때문에 `variant` 타입을 추론할 수 없기 때문이다.

이럴 땐 `variant` 타입을 필수 값으로 지정해도 되지만, 나는 그냥 아래와 같이 primary 타입을 추가하였다. 어차피 모든 버튼 Style에서 primary Variant를 갖고 있기 때문에 문제되지 않는다.

```typescript
export interface ButtonProps<Style extends ButtonStyle> extends HTMLAttributes<HTMLButtonElement> {
  styleType: Style
  variant?: ButtonVariant<Style> | 'primary'
}
```
