Home Type Script | Generics
Post
Cancel

Type Script | Generics

목차

  1. Generics
  2. 제네릭의 Hello World (Hello World of Generics)
  3. 제네릭 타입 변수 작업 (Working with Generic Type Variables)
  4. 제네릭 타입 (Generic Types)
  5. 제네릭 클래스 (Generic Classes)
  6. 제네릭 제약조건 (Generic Constraints)
  7. 제네릭 제약조건에서 타입 매개변수 사용 (Using Type Parameters in Generic Constraints)


1. Generics

단일 타입이 아닌 다양한 타입에서 작동하는 컴포넌트를 작성할 수 있습니다. 사용자는 제네릭을 통해 여러 타입의 컴포넌트나 자신만의 타입을 사용할 수 있습니다.

2. 제네릭의 Hello World (Hello World of Generics)

1
2
3
4
5
6
7
8
9
function identity(arg: number): number {
  // 특정 타입 기입
  return arg;
}

function identity(arg: any): any {
  // any 타입
  return arg;
}

제네릭이 없다면 위의 예시처럼 특정 타입을 주거나 any 타입을 사용하여 함수를 기술할 수 있음

대신에 우리는 무엇이 반환되는지 표시하기 위해 인수의 타입을 캡처할 방법이 필요하기 때문에 여기서는 값이 아닌 타입에 적용되는 타입 변수 를 사용할 것입니다.

1
2
3
function identity<T>(arg: T): T {
  return arg;
}

⇒ 위의 코드는 인수와 반환 타입이 같은 타입을 사용하고 있는 것을 확인할 수 있으며 이를 통해 타입 정보를 함수의 한쪽에서 다른 한쪽으로 운반할 수 있게끔 함

이 버전의 identity 함수는 타입을 불문하고 동작하므로 제네릭이라 할 수 있음

any를 쓰는 것과는 다르게 인수와 반환 타입에 number를 사용한 첫 번째 identity 함수만큼 정확함 (즉, 어떤 정보도 잃지 않음)

일단 제네릭 identity 함수를 작성하고 나면, 두 가지 방법 중 하나로 호출할 수 있음

방법 1) 은 함수에 타입 인수를 포함한 모든 인수를 전달하는 방법

1
2
let output = identity<string>("myString");
// 출력 타입은 'string'입니다.

방법 2)은 아마 가장 일반적인 방법입니다. 여기서는 타입 인수 추론을 사용 — 즉, 우리가 전달하는 인수에 따라서 컴파일러가 Type의 값을 자동으로 정하게 하는 것

1
let output = identity("myString"); // 출력 타입은 'string'입니다.

타입 인수를 꺾쇠괄호(<>)에 담아 명시적으로 전달해 주지 않고 컴파일러는 값인 "myString"를 보고 그것의 타입으로 Type를 정함

인수 추론은 코드를 간결하고 가독성 있게 하는 데 있어 유용하지만 더 복잡한 예제에서 컴파일러가 타입을 유추할 수 없는 경우엔 명시적인 타입 인수 전달이 필요할 수도 있음

3. 제네릭 타입 변수 작업 (Working with Generic Type Variables)

제네릭을 사용하기 시작하면, 제네릭 함수를 만들 때, 컴파일러가 함수 본문에 제네릭 타입화된 매개변수를 쓰도록 강요함 즉, 이 매개변수들은 실제로 any 나 모든 타입이 될 수 있는 것처럼 취급할 수 있게 됨

1
2
3
4
5
function identity<T>(arg: T): T {
  console.log(arg.length); // 오류: T에는 .length 가 없습니다.
  // Property 'length' does not exist on T 'T'.
  return arg;
}

위 제네릭 코드에서 함수의 인자와 반환 값에 대한 타입을 정하진 않았지만, 입력 값으로 어떤 타입이 들어왔고 반환 값으로 어떤 타입이 나가는지 알 수 있음

하지만 현재 인자인 arg에 문자열이나 배열이 들어와도 아직은 컴파일러 입장에서 .length를 허용할 순 없음

(number가 들어왔을 때는 .length 코드가 유효하지 않기 때문)

1
2
3
4
5
6
7
8
9
10
11
// 방법 1
function identity<T>(arg: T[]): T[] {
  console.log(arg.length); // 배열은 .length를 가지고 있습니다. 따라서 오류는 없습니다.
  return arg;
}

// 방법 2
function identity<T>(arg: Array<T>): Array<T> {
  console.log(arg.length); // 배열은 .length를 가지고 있습니다. 따라서 오류는 없습니다.
  return arg;
}

identity의 타입을 “제너릭 함수는 타입 매개변수 T와 T 배열인 인수 arg를 취하고 T 배열을 반환한다.”라고 읽을 수 있음

만약 우리가 number 배열을 넘기면 T가 number에 바인딩 되므로 함수는 number 배열을 얻게 됨

전체 타입변수를 쓰는 것보다 하나의 타입으로써 제네릭 타입변수 T를 사용하는 것은 굉장한 유연함을 제공함

4. 제네릭 타입 (Generic Types)

함수 자체의 타입과 제네릭 인터페이스를 만드는 방법

  • 함수 자체의 타입
1
2
3
4
5
6
7
8
9
10
11
12
function identity<T>(arg: T): T {
  return arg;
}

// 1. 타입 매개변수가 먼저 나열되는 형태
let myIdentity: <T>(arg: T) => T = identity;

// 2. 타입의 제네릭 타입 매개변수에 다른 이름 사용이 가능함
let myIdentity: <Input>(arg: Input) => Input = identity;

// 3. 제네릭 타입을 객체 리터럴 타입의 함수 호출 시그니처로 작성 가능
let myIdentity: { <T>(arg: T): T } = identity;
  • 제네릭 인터페이스를 작성 가능(앞서 예제의 객체 리터럴을 인터페이스로 가져옴)
1
2
3
4
5
6
7
8
9
interface GenericIdentityFn {
  <T>(arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn = identity;
  • 제네릭 매개변수를 전체 인터페이스의 매개변수로 옮기고 싶은 경우, 이를 통해 제네릭 타입을 확인할 수 있음
    (예 - Dictionary 가 아닌 Dictionary<string>)
  • 이렇게 하면 인터페이스의 다른 모든 멤버가 타입 매개변수를 볼 수 있음
1
2
3
4
5
6
7
8
9
10
interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
// 시그니처가 사용할 것을 효과적으로 제한할 특정한 타입 인수가 필요함(여기서는 number)
  • 타입 매개변수를 호출 시그니처에 바로 넣을 때와 인터페이스 자체에 넣을 때를 이해하는 것은 타입의 제네릭 부분을 설명하는 데 도움이 됨

5. 제네릭 클래스 (Generic Classes)

제네릭 클래스와 제네릭 인터페이스는 형태가 비슷함 제네릭 클래스는 클래스 이름 뒤에 꺾쇠괄호(<>) 안쪽에 제네릭 타입 매개변수 목록을 가짐

1
2
3
4
5
6
7
8
9
10
class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

이것은 GenericNumber 클래스의 문자 그대로 사용하지만 number 타입만 쓰도록 제한하는 것은 없음 대신 string이나 훨씬 복잡한 객체를 사용할 수 있음

1
2
3
4
5
6
7
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
  return x + y;
};

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

인터페이스와 마찬가지로 클래스 자체에 타입 매개변수를 넣으면 클래스의 모든 프로퍼티가 동일한 타입으로 동작하는 것을 확인할 수 있음

클래스는 정적 측면과 인스턴스 측면의 두 가지 타입을 가짐 제네릭 클래스는 정적 측면이 아닌 인스턴스 측면에서만 제네릭이므로 클래스로 작업할 때 정적 멤버는 클래스의 타입 매개변수를 쓸 수 없음

6. 제네릭 제약조건 (Generic Constraints)

1
2
3
4
function loggingIdentity<T>(arg: T): T {
  console.log(arg.length);
  //Property 'length' does not exist on type 'T'.
}

모든 타입에서 .length프로퍼티를 가질 수 없기 때문에 T이 무엇이 될 수 있는지에 대한 제약 조건을 나열해야함

이를 위해 우리의 제약조건이 명시하는 인터페이스를 만듭니다. 여기 하나의 프로퍼티 .length를 가진 인터페이스를 생성하였고, 우리의 제약사항을 extends 키워드로 표현한 인터페이스를 이용해 명시함

1
2
3
4
5
6
7
8
interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

제네릭 함수는 이제 제한되어 있기 때문에 모든 타입에 대해서는 동작하지 않음
(대신 필요한 프로퍼티들이 있는 타입의 값을 전달해야함)

1
2
3
4
loggingIdentity(3);
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

loggingIdentity({ length: 10, value: 3 });

7. 제네릭 제약조건에서 타입 매개변수 사용 (Using Type Parameters in Generic Constraints)

다른 타입 매개변수로 제한된 타입 매개변수를 선언할 수 있음
이름이 있는 객체에서 프로퍼티를 가져오고 싶은 경우를 예로 들 때 실수로 obj에 존재하지 않는 프로퍼티를 가져오지 않도록 하기 위해 두 가지 타입에 제약조건을 두어야 함

1
2
3
4
5
6
7
8
9
10
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
getProperty(x, "m");
Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.





출처

Typescript 공식문서 - Generics
타입스크립트 핸드북

This post is licensed under CC BY 4.0 by the author.