TypeScript 핸드북 6 - Generic

Generics

소개

소프트웨어 엔지니어링의 주요한 부분은, 일관되고 잘 정의된 API를 보유할 뿐만 아니라 재사용 가능한 구성 요소를 구축하는 것입니다. 내일의 데이터뿐만 아니라 오늘의 데이터를 처리할 수있는 구성 요소는 대형 소프트웨어 시스템 구축에 높은 유연성을 부여합니다.

C# 및 Java와 같은 언어에서 재사용 가능한 구성 요소를 작성하기 위한 기본 도구 중 하나는 Generic입니다. 즉, 하나가 아닌 다양한 타입에서 작동할 수있는 구성 요소를 만들 수 있어야합니다. 사용자는 이를 통해 다양한 구성 요소를 사용하고 또한 자신만의 타입을 사용할 수도 있습니다.

Hello World of Generics

우선, Generic의 “hello world”(identity 함수)를 작성해 보겠습니다. identity 함수는 전달된 모든 것을 되돌리는 함수입니다. 이 함수는 echo 명령과 비슷한 방식으로 작동합니다.

Generic이 없다면 identity 함수에 특정 타입을 부여해야합니다.

1
2
3
function identity(arg: number): number {
return arg;
}

또는 any 타입을 사용하여 Identity 함수를 작성할 수 있습니다 :

1
2
3
function identity(arg: any): any {
return arg;
}

any를 사용하는 것은 arg의 타입이 모든 타입을 수용하게 하는 확실한 방법 이지만, 실제로 함수가 리턴할 때 그 타입이 무엇인지에 대한 정보를 잃어 버립니다. number가 입력 됐지만, any 타입이 리턴 될 수 있다는 것입니다.

대신, 우리는 리턴되는 값을 나타내는 데 사용할 수있는 방식으로 파라미터 타입을 Capture하는 방법이 필요합니다.

여기서 Type 변수 을 사용합니다. Type 변수는 값이 아닌 타입에서 작동하는 특별한 종류의 변수입니다.

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

이제 identity 함수에 Type 변수 T를 추가했습니다. 이 T는 사용자가 제공 한 타입 (예 :number)을 Capture하여 나중에 해당 정보를 사용할 수 있도록합니다. 그리고 리턴 타입으로 T를 다시 사용합니다. 인수와 리턴 타입에 동일한 타입이 사용된 것을 볼 수 있습니다. 이를 통해 우리는 함수의 한쪽에서 다른쪽으로 타입 정보를 트래핑할 수 있습니다.

이 버전의 identity 함수는 Generic 타입이라고 말합니다. any를 사용하는 것과는 달리, 파라미터와 리턴 값에 숫자를 사용하는 첫 번째identity 함수로서 정확합니다 (즉, 어떤 정보도 잃지 않습니다).

Generic identity 함수를 작성한 후에는 두 가지 방법 중 하나로 호출할 수 있습니다. 첫 번째 방법은 Type 파라미터를 포함한 모든 파라미터를 함수에 전달하는 것입니다.

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

위 코드에서는 함수 호출에 대한 파라미터 중 하나 인 Tstring으로 명시적으로 설정했습니다. ()보다는 파라미터를 중심으로 <>를 사용했습니다.

두 번째 방법은 가장 일반적인 방법이기도 합니다. 파라미터 타입 추론을 사용합니다. 즉, 컴파일러가 전달하는 파라미터의 타입에 따라 자동으로 T의 값을 설정합니다.

1
let output = identity("myString"); // 아웃풋 타입은 'string' 타입일 것입니다.

꺽쇠 괄호 (<>)에 명시적으로 타입을 전달할 필요가 없음을 명심하세요. 컴파일러는 단지 "myString" 값을보고, T를 그 타입으로 설정합니다. 파라미터 타입 추론은 코드를 더 짧고 가독성있게 유지하는 유용한 툴이 될 수 있지만, 복잡한 예제에서는 컴파일러가 타입을 추론하지 못했을 때 이전 예제에서 했던 것처럼 타입 파라미터를 명시 적으로 전달해야할 수도 있습니다 .

Generic Type 변수로 작업하기

Generic을 사용하여 identity와 같은 Generic 함수를 만들면 컴파일러는 함수의 몸체에 일반적으로 타입이 지정된 파라미터를 올바르게 적용하도록 강제합니다. 즉, 실제로 이러한 파라미터 변수를 모든 타입이 될 수있는 것처럼 취급합니다.

앞에서 우리의 identity 함수를 보겠습니다.

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

각 호출과 함께 콘솔에 파라미터 arg의 길이를 기록하고 싶다면 어떻게해야할까요?

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T는 .length를 가지고 있지 않습니다.
return arg;
}

컴파일러는 “arg.length 멤버를 사용하고 있지만 arg에는 이 멤버가 있다고는 말할 수 없다”는 오류를 출력합니다. 이러한 Type 변수에 모든 타입이 사용될수 있으므로 이전에 이 함수를 사용했던 사람이 number를 전달할 수 있지만 .length 멤버가 없는 것으로 나타납니다.

실제로 이 함수가 T 대신 T 배열로 직접 작업한다고 가정 해 봅시다. 우리는 배열을 다루기 때문에 .length 멤버를 사용할 수 있어야 합니다. 우리는 다른 타입의 배열을 생성하는 것처럼 똑같이 설명할 수 있습니다.

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array는 .length를 가지고 있으므로 더이상 error가 아닙니다.
return arg;
}

loggingIdentity의 타입을 다음과 같이 읽을 수 있습니다 : “Generic 함수 loggingIdentity는 타입 파라미터 변수 T를 취하고 파라미터 argT의 배열이며, T의 배열을 반환합니다.” 숫자 배열을 건네 주면, Tnumber에 묶이기 때문에 숫자 배열을 리턴할 것입니다. 이렇게 하면 전체 타입보다는 일반 타입 변수 T를 사용중인 타입의 일부로 사용할 수 있으므로 유연성이 향상됩니다.

우리는 다른 대안으로 샘플 예제를 다음과 같이 작성할 수 있습니다.

1
2
3
4
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array는 .length를 가지고 있으므로 더이상 error가 아닙니다.
return arg;
}

이런 방식에 대해 다른 언어로 된 비슷한 스타일을 잘 알고있을 수 있습니다. 다음 섹션에서는 Array<T>와 같이 자신 만의 Generic 타입을 만드는 방법에 대해 다룰 것입니다.

Generic 타입

이전 섹션에서는 다양한 타입의 함수를 처리하는 Generic identity 함수를 만들었습니다. 이 섹션에서는 Generic 함수 타입과 Generic 인터페이스를 만드는 방법을 살펴 보겠습니다.

Generic 함수 타입은 함수 선언과 마찬가지로 타입 파라미터가 먼저 나열된 Non-generic 함수의 형식과 같습니다.

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;

타입의 수와 타입 변수의 사용법이 일치 하면, 타입에서 Generic Type 파라미터에 다른 이름을 사용할 수도 있습니다.

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;

Generic 타입을 객체 리터럴 형식의 Call signature로 쓸 수도 있습니다.

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;

첫 번째 Generic 인터페이스를 작성해 보겠습니다. 앞의 예제에서 객체 리터럴을 가져 와서 인터페이스로 옮깁니다.

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;

비슷한 예를 들어, Generic 파라미터를 전체 인터페이스의 파라미터로 이동할 수 있습니다. 이렇게 하면 Generic이 어떤 타입인지를 알 수 있습니다 (예 :Dictionary가 아닌 Dictionary<string>). 이것은 인터페이스의 다른 모든 구성원이 Type 파라미터를 볼 수 있다는 의미입니다.

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<number> = identity;

예제가 약간 다른 것으로 변경되었습니다. Generic 함수를 설명하는 대신 Generic 타입의 일부인 Non-generic 함수 Signature를 갖게 되었습니다. GenericIdentityFn을 사용할 때 해당 타입 파라미터 (여기서는 number)를 지정 해야하며 기본 Call signature가 사용할 항목을 효과적으로 잠글 수 있습니다. 타입 파라미터를 Call signature에 직접 적용할 시기와 인터페이스 자체에 넣을 시기를 이해하면 타입의 어떤 측면이 Generic 인지 설명하는 데 도움이됩니다.

Generic 인터페이스 외에도 Generic 클래스를 만들 수도 있습니다. 하지만 Generic Enum 및 네임 스페이스는 만들수 없다는것을 유의하세요.

Generic 클래스

Generic 클래스는 Generic 인터페이스와 모양이 비슷합니다. Generic 클래스는 클래스 이름 다음에 꺾쇠 괄호 (<>)로 묶인 Generic Type 파라미터 목록을 갖습니다.

1
2
3
4
5
6
7
8
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

이 예제는 GenericNumber 클래스를 그대로 사용 하지만, number 타입만 사용하도록 하는 제한이 없다는 것을 눈치 챘을 것입니다. 대신에 string 또는 더 복잡한 객체를 사용할 수 있습니다.

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

인터페이스와 마찬가지로 Type 파라미터를 클래스 자체에 두면 클래스의 모든 프로퍼티가 동일한 타입으로 작동하는지 확인할 수 있습니다.

클래스에 대한 섹션에서 다뤘 듯이 클래스에는 Static 측면과 Instance 측면의 두 가지 유형이 있습니다. Generic 클래스는 Static 측면보다는 Instance 측면에서 Generic 하기 때문에 클래스로 작업할 때 Static 멤버는 클래스의 Type 파라미터를 사용할 수 없습니다.

Generic Constraint

처음 예제에서 타입의 집합에 어떤 메서드가 있는지에 대해 이미 알고 있고, 타입의 집합에서 작동하는 Generic 함수를 작성하는 경우가 있었습니다. loggingIdentity 예제에서 arg.length 프로퍼티에 접근할 수 있기를 원했지만 컴파일러는 모든 타입이 .length 프로퍼티을 가지고 있음을 증명할 수 없기 때문에 이러한 가정을 할 수 없다고 경고 했습니다.

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T 는 .length를 가지고 있지 않습니다.
return arg;
}

이 함수가 임의의 모든 타입으로 작업하는 대신에, .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;
}

Generic 함수는 이제 제한되어 있으므로 더 이상 모든 타입에서 작동하지 않습니다.

1
loggingIdentity(3); // Error, number는 .length 프로퍼티를 가지고 있지 않습니다.

대신, 필수 프로퍼티가 있는 타입 값을 전달해야합니다.

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

Generic Constraint에서 Type 파라미터 사용하기

다른 Type 파라미터에 의해 제한되는 Type 파라미터를 선언할 수 있습니다. 예를 들어, 이름이 있는 객체로부터 프로퍼티를 얻고 싶을 수 있습니다. 우리는 obj에 존재하지 않는 프로퍼티를 우연히 잡아 내지 않도록하기 위해 두 가지 타입 사이에 제약 조건을 적용할 것입니다.

1
2
3
4
5
6
7
8
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"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

Generic에서 클래스 타입 사용하기

TypeScript에서 Generic을 사용하여 팩토리를 생성할 때, 생성자 함수를 사용하여 클래스의 타입을 참조 해야합니다. 예를 들어,

1
2
3
function create<T>(c: {new(): T; }): T {
return new c();
}

고급 예제는 프로토 타입 속성을 사용하여 생성자 함수와 클래스 유형의 인스턴스 사이의 관계를 추론하고 제한합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!

이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://www.typescriptlang.org/docs/handbook/generics.html]

참고

공유하기