Typescript Generic

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

C # 및 Java와 같은 언어에서 재사용 가능한 구성 요소를 작성하기위한 도구 상자의 주요 도구 중 하나는 제네릭 즉 단일 type이 아닌 다양한 type에서 작동 할 수있는 구성 요소를 생성하는 것입니다. 이를 통해 사용자는 이러한 구성 요소를 사용하고 자신의 유형을 사용할 수 있습니다.

Hello World of Generic

generic에서 “Hello world”에 해당되는 identity function으로 시작해 봅시다. indentity function은 전달받은 것으로 반환하는 함수 입니다.

generic가 없다면 identity 함수에 특정 type을 부여해야합니다.

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

또는 any type을 사용하여 ID 함수를 기술 할 수 있습니다.

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

any를 사용하는 것은 확실히 arg의 타입에 대한 모든 타입을 받아 들일 것이지만, 실제로 함수가 리턴 할 때 그 type이 무엇인지에 대한 정보를 잃어 버리게됩니다. number를 전달하면, 우리가 가지고있는 유일한 정보는 any 타입이 리턴 될 수 있다는 것입니다.

대신, 우리는 반환되는 것을 나타내는 데 사용할 수있는 방식으로 argument type을 캡처하는 방법이 필요합니다. 여기서 type variable는 value가 아닌 type에서 작동하는 특별한 종류의 변수입니다.

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

이제 identity 함수에 tye variable T를 추가했습니다. 이 T는 type (예 : number)을 캡처하여 나중에 해당 정보를 사용할 수 있도록합니다. 여기에서 T를 리턴 type으로 다시 사용합니다.

다양한 type에서 작동하므로 identity 함수가 generic하다고 말흡니다. any를 사용하는 것과는 달리, argument와 return type에 number를 사용하는 첫 번째 identity 함수만큼 정확합니다(즉, 정보를 잃지 않습니다).

generic indentity 함수를 작성한 후에는 두 가지 방법 중 하나로 호출 할 수 있습니다. 첫 번째 방법은 type argument를 포함한 모든 argument를 함수에 전달하는 것입니다.

1
let output = identity<string>("myString");

<>를 사용하여 stringT에 argument로 명시적으로 설정합니다.

두 번째 방법은 가장 일반적인 방법이기도 합니다. 여기서 우리는 type argument inference을 사용합니다. 컴파일러가 우리가 전달하는 argument의 type에 따라 자동으로 T 값을 설정합니다.

1
let output = identity("myString");

꺽쇠 괄호 (<>) 안에 명시 적으로 type을 전달할 필요가 없었습니다. 컴파일러는 방금 "myString"값을보고 T를 해당 유형으로 설정합니다. type argument inference은 코드를 더 짧고 가독성있게 유지하는 유용한 도구가 될 수 있지만, 보다 복잡한 예제에서 컴파일러가 타입을 추론하지 못하면 앞의 예제에서 했던 것처럼 타입 인자를 명시 적으로 전달해야 할 수도 있습니다 .

Working with Generic Type Variable

함수 본문 안에서 type parameter를 올바르게 사용해야 합니다. 즉, type parameter를 모든 type이 될 수있는 것처럼 취급해야 합니다.

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

arg의 length를 콘솔에 기록하려면 어떻게 해야 할까요?

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}

arg.length 멤버가 존재하는지 알 수 없기 때문에 에러가 발생합니다. type variable은 모든 type에 사용되므로 .length 멤버가 없는 `number’를 전달할 수 있습니다.

우리가 실제로이 함수가 T가 아닌 T의 배열을 작업한다고 가정 해 보겠습니다. 우리가 배열을 다루기 때문에, .length 멤버를 사용할 수 있어야 합니다.

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}

loggingIdentity는 “generic function loggingIdentity가 type parameter TT의 배열을 argument arg로 받고 T의 배열을 반환하는” type입니다.

number 배열을 전달하면 Tnumber에 바인드하므로 number 배열을 되돌려 받습니다.

이렇게 하면 전체 type보다는 우리가 작업하는 type의 일부로 generic type variable T를 사용할 수 있으므로 유연성이 향상됩니다.

다음과 같이 작성할 수도 있습니다.

1
2
3
4
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

Generic Type

함수 type과 generic 인터페이스를 만드는 방법에 대해 살펴 보겠습니다.

generic 함수의 문법은 type parameter를 <> 기술합니다.

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

generic type을 object literal type의 call signature로 기술할 수도 있습니다.

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

예제에서 object literal을 가져 와서 인터페이스로 옮깁니다.

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 parameter를 전체 인터페이스의 parameter로 옮길 수도 있습니다. 이렇게하면 어떤 type이 generic이 되어 있는지 확인할 수 있습니다 (예 : Dictionary이 아닌 Dictionary<string>). 이렇게하면 인터페이스의 다른 모든 구성원이 type parameter를 볼 수 있습니다.

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;

tye parameter를 call signature에 직접 적용 할때와 인터페이스 자체에 넣을 때를 이해하면 type의 어떤 측면이 generic인지 설명하는 데 도움이됩니다.

generic 인터페이스 외에도 generic 클래스를 만들 수도 있습니다. generic enum 및 generic namespace를 만들 수는 없습니다.

Generic Class

generic 클래스에는 클래스 이름 뒤에 꺾쇠 괄호 (<>)로 묶인 geenric type parameter 목록이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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; };
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

클래스에 대한 섹션에서 살펴본 것처럼 클래스에는 정적 측면과 인스턴스 측면의 두 가지 유형이 있습니다. generic 클래스는 정적 측면보다는 인스턴스 측면에서만 generic이므로 클래스를 사용하여 작업 할 때 정적 멤버는 클래스의 type parameter를 사용할 수 없습니다.

Generic Constraint

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // 에러
return arg;
}

임의의 모든 type으로 작업하는 대신이 함수가 .length 속성도 가진 type에서 작동하도록 제한하고 싶습니다. 그렇게하기 위해서 우리는 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);
return arg;
}

제네릭 함수가 이제 제한되어 있기 때문에 더 이상 모든 type에서 작동하지 않습니다.

1
loggingIdentity(3); // 에러

대신 모든 필수 속성이있는 type의 값을 전달해야합니다.

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

generic constraint에서 type parameter 사용

다른 type parameter에 의해 제한되는 type parameter를 선언 할 수 있습니다. 예를 들어 여기서는 두 개의 객체를 가져 와서 하나에서 다른 객체로 속성을 복사하려고합니다. 실수로 source의 추가 속성을 작성하지 않도록하려면 두 가지 type간에 제약 조건을 적용합니다.

1
2
3
4
5
6
7
8
9
10
11
function copyFields<T, U extends T>(source: T, target: U): U {
for (let id in source) {
target[id] = source[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields({ b: 10, d: 20 }, x);
copyFields({ Q: 90 }, x); // 에러

generic에서 class type 사용

generics를 사용하여 팩토리를 생성 할 때 constructor 함수를 사용하여 클래스 유형을 참조해야합니다. 예를 들어,

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

고급 예제는 prototype 속성을 사용하여 constructor 함수와 class type의 인스턴스 측면 사이의 관계를 추론하고 제한합니다.

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
27
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 findKeeper<A extends Animal, K> (a: {new(): A;
prototype: {keeper: K}}): K {
return a.prototype.keeper;
}
findKeeper(Lion).nametag; // typechecks!
Share