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

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

·

3 min read

YEONSUI 프로젝트를 진행하며 버튼 컴포넌트에 제네릭을 사용한 적 있다. (자세한 내용은 지난 포스트를 참고!)

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

🔵 제네릭(Generics)이란?

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

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

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

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

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

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

🔵 제네릭 제약 조건

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

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

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

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 타입을 사용할 수도 있지 않을까?

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

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

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

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 타입을 검사하지 않기 때문에 타입 에러가 발생하지 않는다.

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와 달리 타입 검사를 하기 때문에 에러를 발견할 수 있다.


레퍼런스

제네릭 | 타입스크립트 핸드북