본문 바로가기
Typescript

Typescript | 제네릭, 타입 변수 응용, map, forEach 메서드 타입 정의

by 리잼 2024. 11. 3.
반응형

제네릭

함수나 인터페이스, 타입 별칭, 클래스 등을 다양한 타입과 함께 동작하도록 만들어 주는 타입스크립트의 기능 중 하나

제네릭이 필요한 상황

  • 다양한 타입의 매개변수를 받고 해당 매개변수를 그대로 반환하는 함수가 하나 필요하다고 가정
function func(value: any) {
  return value;
}

let num = func(10);
// any 타입

let str = func("string");
// any 타입
  • 다양한 타입의 매개변수를 제공받아야 하기 때문에 매개변수 value의 타입을 일단 any 타입으로 지정한다.
  • 이 함수는 인수로 전달한 값을 그냥 그대로 반환하는 단순한 함수이다.
    따라서 변수 num에는 10이 저장되고 변수 str에는 “string”이 저장된다.
    그런데 현재 num과 str의 타입은 any 타입이다.
    func 함수의 반환값 타입이 return 문을 기준으로 추론되었기 때문
  • 이렇게 함수 호출 결과를 저장하는 num, str 등의 변수가 any 타입으로 추론되면 다음과 같은 문제점이 발생한다.
function func(value: any) {
  return value;
}

let num = func(10);
let str = func("string");

num.toUpperCase()
  • num에는 분명 Number 타입의 값 10이 저장되어 있다.
  • 그러나 any 타입으로 추론되어 버렸기 때문에 toUpperCase 등의 String 타입의 메서드를 사용해도
    타입스크립트가 오류를 감지하지 못한다.
  • 이 코드는 런타임 오류를 발생한다 이러한 문제를 제네릭 함수가 해결할 수 있다.

제네릭 ( Generic ) 함수

제네릭 함수는 두루두루 모든 타입의 값을 다 적용할 수 있는 범용적인 함수이다.

제네릭 함수는 아래와 같이 선언할 수 있다.

function func<T>(value: T): T {
  return value;
}

let num = func(10);
// number 타입
  1. 함수 이름 뒤에 꺽쇠를 열고 타입을 담는 변수인 타입 변수 T를 선언한다.
  2. 그리고 매개변수와 반환값의 타입을 이 타입변수 T로 설정한다.
  • T에 어떤 타입이 할당될 지는 함수가 호출될 때 결정된다.
  • func(10) 처럼 Number 타입의 값을 인수로 전달하면 매개변수 value에 Number 타입의 값이 저장되면서
    T가 Number 타입으로 추론된다. 이때 func 함수의 리턴 값 또한 Number 타입이 된다.

제네릭 함수를 호출할 때 아래와 같이 타입 변수에 할당할 타입을 직접 명시하는 것도 가능하다.

function func<T>(value: T): T {
  return value;
}

let arr = func<[number, number, number]>([1, 2, 3]);

타입 변수 응용

4가지의 사례를 살펴보며 제네릭을 더 잘 활용하는 방법을 알아본다.

사례1

  • 만약 2개의 타입 변수가 필요한 상황이라면 다음과 같이 T, U 처럼 2개의 타입 변수를 사용해도 된다.
// T는 string, U는 number로 추론된다.
function swap<T, U>(a: T, b: U) {
  return [b, a];
}

const [a, b] = swap("1", 2);

사례2

  • 다양한 배열 타입을 인수로 받는 제네릭 함수를 만들어야 한다면 다음과 같이 할 수 있다.
function returnFirstValue<T>(data: T[]) {
  return data[0];
}

let num = returnFirstValue([0, 1, 2]);
// number

let str = returnFirstValue([1, "hello", "mynameis"]);
// number | string
  • 함수 매개변수 data의 타입을 T[]로 설정했기 때문에 배열이 아닌 값은 인수로 전달할 수 없게 된다.
    배열을 인수로 전달하면 T는 배열의 요소 타입으로 할당된다.
  • 첫번째 호출에서는 인수로 Number[] 타입의 값을 전달했으므로 이때의 T는 Number 타입으로 추론된다.
    이때의 함수 반환값 타입은 Number 타입이 된다.
  • 두번째 호출에서는 인수로 (String | Number)[] 타입의 값을 전달했으므로 이때의 T는 String | Number 타입으로 추론된다.
    이때의 함수 반환값 타입은 String | Number 타입이 된다.

사례3

  • 위 사례에서 만약 반환값의 타입을 배열의 첫번째 요소의 타입이 되도록 하려면 다음과 같이 튜플 타입과 나머지 파라미터를 이용하면 된다.
function returnFirstValue<T>(data: [T, ...unknown[]]) {
  return data[0];
}

let str = returnFirstValue([1, "hello", "mynameis"]);
// number
  • 함수 매개변수의 타입을 정의할 때 튜플 타입을 이용해 첫번째 요소의 타입은 T
    그리고 나머지 요소의 타입은 …unknown[] 으로 길이도 타입도 상관 없도록 정의한다.
  • 함수를 호출하고 [1, “hello”, “mynameis”] 같은 배열 타입의 값을 인수로 전달하면
    T는 첫번째 요소의 타입인 Number 타입이 된다. 따라서 함수 반환값 타입또한 Number 타입이 됩니다.

사례4

타입 변수를 제한하는 사례

  • 아래 코드는 타입 변수를 적어도 length 프로퍼티를 갖는 객체 타입으로 제한한 예시이다.
function getLength<T extends { length: number }>(data: T) {
  return data.length;
}

getLength("123");            // ✅

getLength([1, 2, 3]);        // ✅

getLength({ length: 1 });    // ✅

getLength(undefined);        // ❌

getLength(null);             // ❌
  • 타입 변수를 제한할 때에는 확장(extends)을 이용한다.
  • 위와 같이 T extends { length : number } 라고 정의하면 T는 이제 { length : number } 객체 타입의 서브 타입이 된다.
    바꿔말하면 이제 T는 무조건 Number 타입의 프로퍼티 length 를 가지고 있는 타입이 되어야 한다는 것.
    따라서 이렇게 extends를 이용해 타입 변수를 제한하면 아래와 같은 결과가 나타난다.
  1. 1번 호출은 인수로 length 프로퍼티가 존재하는 String 타입의 값을 전달 했으므로 허용된다.
  2. 2번 호출은 인수로 length 프로퍼티가 존재하는 Number[] 타입의 값을 전달 했으므로 허용된다.
  3. 3번 호출은 인수로 length 프로퍼티가 존재하는 객체 타입의 값을 전달 했으므로 허용된다.
  4. 4번 호출은 인수로 undefined을 전달했으므로 오류가 발생한다.
  5. 5번 호출은 인수로 null을 전달했으므로 오류가 발생한다.

 

map 메서드 타입 정의

JS의 배열 메서드 Map은 다음과 같이 원본 배열의 각 요소에 콜백함수를 수행하고 반환된 값들을 모아 새로운 배열로 만들어 반환한다.

let arr = [1,2,3];

function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(callback(arr[i]));
  }
  return result;
}

map(arr, (it) => it.toString());
// string[] 타입의 배열을 반환
// 결과 : ["1", "2", "3"]

 

ForEach 메서드 타입정의

forEach 메서드는 다음과 같이 배열의 모든 요소에 콜백함수를 한 번씩 수행해주는 메서드이다.

let arr = [1,2,3]

function forEach<T>(arr: T[], callback: (item: T) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
  }
}

arr.foreach((it) => console.log(it));
// 1, 2, 3 출력
  • Map과 동일하게 2개의 매개변수를 받는다
  • 첫번째 매개변수 arr에는 순회 대상 배열을 제공받고 두번째 매개변수 callback에는 모든 배열 요소에 수행할 함수를 제공 받는다.
    이때 아까 Map 메서드의 타입 정의와는 달리 forEach 메서드는 반환값이 없는 메서드이므로
    콜백 함수의 반환값 타입을 void로 정의한다.
반응형