제너릭 타입

핵심 포인트

  • 상수 타입 매개변수 const T - TS v5.0에 추가됨

    • 타입을 좁힐 때 사용 (ex: string을 유니온 리터럴로 추론되게 할 때)

  • 타입 매개변수와 제약(extends)을 동일하게 생각하지 마라.

    • 타입 매개변수는 제약에 대입할 수 있는 모든 타입을 의미

    • 타입 매개변수는 제약보다 더 넓은 타입, (제약이 더 좁은 타입이라 생각)

  • 강박적으로 제너릭을 쓸 필요는 없다. 특히, 원시값 타입만 사용한다면 대부분 제약을 걸지 않아도 된다.

제너릭

타입간에 중복을 제거하기 위해 제너릭을 사용

interface Person<N, A> {
  type: 'human',
  race: 'yellow',
  name: N,
  age: A,
}

interface Zero extends Person<'zero', 28> {}
interface Nero extends Person<'nero', 32> {}

// Array 타입도 제너릭
// 제너릭이 없었다면 스트링배열, 넘버배열을 따로 타입을 선언해줬어야 됬을것
interface Array<T> {
  [key: number]: T,
  length: number
  //...
}
  • 표기는 <> 로 하며 인터페이스 이름 바로 뒤에 위치

  • <> 안에 타입 매개변수를 넣어서 사용

  • 제너릭 타입으로 타입간 중복을 줄이고 재사용성을 높힘

제너릭 표기 위치

함수에서 함수 선언문이냐 표현식이냐에 따라 제너릭 표기 위치가 달라짐

함수는 제너릭표기가 인자 앞에 위치한다고 생각

const personFactoryE = <N, A>(name: N, age: A) => {
  ...
}

function personalFactoryD<N,A>(name: N, age: A) {
  ...
}
  • interface 이름<타입 매개변수들> {...}

  • type 이름<타입 매개변수들> = {...}

  • class 이름<타입 매개변수들> {...}

  • function 이름<타입 매개변수들>(...) {...}

  • const 함수이름 = <타입 매개변수들>(...) ⇒ {...}

제너릭 기본값

타입 매개변수에 기본값을 사용할 수 있다.

interface Person<N = string, A = number> {
  type: 'human',
  race: 'yellow',
  Name: N,
  age: A
}

type Person1 = Person; // Person<string, number>
type Person2 = Person<number>; // Person<number, number>
type Person3 = Person<number, boolean>; // Person<number, boolean>

타입 추론

타입스크립트는 제너릭에 직접 타입을 넣지 않아도 추론을 통해 타입을 알아 낼 수 있다.

interface Person<N, A> {
  type: 'human',
  race: 'yellow',
  Name: N,
  age: A
}

const personFactoryE = <N, A = unknown>(name: N, age: A): Person<N,A> => ({
  type: 'human',
  race: 'yellow',
  name,
  age,
})

const zero = personFactoryE('zero', 28); // const zero: Person<string, number>
  • 이처럼 타입스크립트가 추론을 통해 타입을 알아낼 수 있는 경우는 직접 <> 타입을 넣지 않아도 된다.

    • 실제로도 직접 넣지 않는 경우가 더많음

  • 타입스크립트 5.0 버전에서 상수 타입 매개변수 (const T)가 추가됨

상수 타입 매개변수

function values<T>(initial: T[]) {
  return {
    hasValue(value:T) { return initial.includes(value) }
  }
}

const savedValues = values(['a','b','c']); // T는 string[]로 추론됨
savedValues.hasValue('x'); // T가 string으로 추론되기에 'x'를 넣어도 타입 오류가 나지 않음
  • ❓T를 string 대신 'a' | 'b' | 'c' 같은 유니온으로 추론되게 하려면?

    • 타입 매개변수 앞에 const 수식어를 추가하면 타입 매개변수 T를 추론할 때 as const를 붙인 값으로 추론됨

// TS v4.9 
function values<T>(initial: readonly T[]) { //readonly 수식어로 정확한 타입을 추론하게끔 설정
  //...
}

const savedValues = values(['a','b','c'] as const); // as const 접미사로 tuple로 만듬
savedValues.hasValue('x'); // T가 'a' | 'b' | 'c' 이므로 에러 발생

// TS v5.0
function values<const T>(initial: T[]) { // 타입 매개변수 앞에 const 수식어 사용
  //...
}

const savedValues = values(['a', 'b', 'c']);
savedValues.hasValue('x'); // 타입 에러 발생

제너릭에 제약 걸기

타입 매개변수에는 제약(constraint)를 사용 가능

extends 문법으로 타입 매개변수의 제약을 표시

  • 타입의 상속을 의미하던 extends 와는 사용법이 다르므로 구분!!

  • 제약이 걸리면 제약에 어긋나는 타입은 입력할 수 없지만 제약보다 더 구체적인 타입은 입력 가능

    • 이러한 점에서 제약은 기본값과는 다르다.

interface Example<A extends number, B = string> {
  a: A,
  b: B
}
type Usecase1 = Example<string, boolean>; // 타입 에러
type Usecase2 = Example<1, boolean> // number 보다 더 구체적인 타입 입력 가능
type Usecase3 = Example<number>

// 하나의 타입 매개변수가 다른 타입 매개변수의 제약이 될 수 도 있다.
interface Example<A, B extends A> {
 //...
}

‼️ 자주 쓰이는 제약들

<T extends object> // 모든 객체
<T extends any[]> // 모든 배열
<T extends (...args: any) => any> // 모든 함수
<T extends abstract new (...args: any) => any> // 생성자 타입
<T extends keyof any> // string | number | symbol

제너릭 제약을 사용할 때 흔히 하는 실수

  • ❌ 타입 매개변수와 제약을 동일하게 생각하는 것

  • 먼저 타입 매개변수가 제약에 대입할 수 있는 타입인지를 따져보아야 한다.

  • 강박적으로 제너릭을 쓸 필요는 없다. 특히, 원시값 타입만 사용한다면 대부분 제약을 걸지 않아도 된다.

interface VO {
  value: any;
}
// ☠️ 타입 매개변수와 제약을 동일하게 생각해서 발생하는 실수
const returnVO = <T extends VO>(): T => {
  return { value: 'test' }; // Type Error
}

// 제너릭을 제거하면됨
const returnVO = (): VO => {
  return { value: 'test' };
}
  • T 는 정확히 VO가 아니라 VO에 대입할 수 있는 모든 타입을 의미

    • 따라서 { value: string, another: string }도 T가 될 수 있다.

    • 이러면 { value: string } 은 T가 아니다. 따라서 에러가 발생함.

  • 인자값의 타입은 열려있고, 반환값의 타입은 닫혀있다. (공변성, 반공변성 참고)

// ☠️ 타입 매개변수 T 에 boolean 제약이 걸려있고, 함수의 매개변수도 T 타입
function onlyBoolean<T extends boolean>(arg: T = false): T { // 인자 타입에서 에러 발생
  return arg;
}

// onlyBoolean을 유효하게 만들고 싶다면 간단하다. 제너릭을 쓰지 않으면 된다.
function onlyBoolean(arg: boolean = true): boolean {
  return arg;
}
  • never 타입 때문, never는 모든 타입에 대입할 수 있으므로 never extends boolean은 참이다.

  • 따라서 T가 never 일 수 있으므로 false를 기본값으로 넣는 것이 불가능한 것

Last updated