
제네릭은 타입을 불문하고 동작하는 것을 말한다. 즉, 함수에 인수를 넘길 때 타입 인수도 넘기기 때문에 타입에 관한 어떠한 정보도 잃지 않는다. any를 쓰는 것과는 다르다. any를 쓰게 되면 만약 number타입을 넘기더라도 any타입이 반환된다는 정보만 얻을 뿐이다.
function thisIsGeneric<T>(arg: T): T {
return arg;
}
let ouput = thisIsGeneric<string>('mystring');
console.log(ouput); // 'mystring'
타입 인수를 넘길 때는 <> 이 괄호안에 표기하면 된다.
제네릭 함수를 호출하는 두 번째 방법이다. 위의 예제와는 다르게 타입 인수를 넘겨주지 않고 컴파일러가 전달하는 인수를 보고 이를 추론한다.
function thisIsGeneric<T>(arg: T): T {
return arg;
}
let ouput = thisIsGeneric('mystring'); // 호출할 때 타입 인수를 넘기지 않는다
타입 인수 추론은 코드를 가독성있고 간결하게 하지만 컴파일러가 타입을 추론할 수 없는 복잡한 상황에서는 타입 인수를 전달하는 것이 좋다.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // 에러, T에는 .length 가 없습니다.
return arg;
}
위의 예제를 살펴보자. 넘긴 인수에 length 메서드를 사용했는데, 만약 타입 인수가 number라면 이는 에러를 일으킬 것이다. 따라서 애초에 에러가 발생하므로 이는 비문이다. 이를 맞게 하기 위해서는 다음과 같이 해야한다.
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // 에러, T에는 .length 가 없습니다.
return arg;
}
T[]는 배열이므로 length 메서드를 가지고 있다.
제네릭 함수의 타입을 선언할 때는 비-제네릭 함수와 비슷하게 하면 된다.
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
타입 변수의 수, 사용 방식에 따라 타입 매개변수에 다른 이름을 사용할 수도 있다.
let myIdentity: <U>(arg: U) => U = identity;
객체 리터럴 방식으로 나타낼 수 있다.
let myIdentity: { <T>(arg: T): T } = identity;
그럼 제네릭 인터페이스를 작성해보자. 위의 객체 리터럴을 인터페이스로 사용했다.
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
더 나아가, 제네릭 타입 매개변수를 인터페이스의 매개변수로 사용할 수도 있다. 다음과 같이 말이다.
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
제네릭 클래스는 제네릭 인터페이스랑 형태가 비슷하다. 클래스 이름 뒤 <>안에 타입 매개변수를 전달하고 클래스 블럭 구문에 타입 매개변수 목록들을 가진다.
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; };
클래스는 static, 인스턴스 두가지 타입을 가진다. 제네릭 클래스는 인스턴스의 기준에서만 제네릭이므로 제네릭을 클래스로 작업할 때 정적 프로퍼티는 클래스의 타입 매개변수를 사용할 수 없다.
앞에서 우리는 아래와 같은 예제를 살펴보았다.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // 에러, T에는 .length 가 없습니다
return arg;
}
만약 length 프로퍼티를 가진 타입으로 제약 조건을 걸고 싶다면 extends 키워드로 인터페이스를 사용하면 된다.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity(3); // 에러, number는 .length 프로퍼티가 없습니다
loggingIdentity({length: 10, value: 3}); // 성공
다른 타입 매개변수를 활용해 타입 매개변수를 제한할 수도 있다.
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"); // 오류:
K를 T의 key 타입(keyof)으로 제한했다. 이렇게 제약 조건을 걸어 멍청한 실수를 방지할 수 있다.
팩토리를 사용할 때 생성자 함수에서 클래스 타입을 참조해야 한다. 여기서 팩토리가 뭐냐하면 객체 생성을 단순하게 하여 반복적인 객체들을 생성할 때 사용하는 객체 지향 프로그래밍에서 유래된 디자인 패턴이다.
ES5
function Person(name) {
this.name = name
}
Person.factory = function(name) {
return new Person(name)
}
var ashnamuh = Person.factory('ashnamuh')
console.log(ashnamuh.name) // ashnamuh
ES6
class Animal {
constructor() {
this.type = 'animal'
}
static factory(type) {
switch(type) {
case 'Lion':
return new Lion()
case 'Cat':
return new Cat()
default:
throw new Error('Invalid type!')
}
}
}
class Lion extends Animal {
constructor() {
super()
}
}
class Cat extends Animal {
constructor() {
super()
}
}
const lion = Animal.factory('Lion')
const cat = Animal.factory('Cat')
TypeScript
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();
}
new() => A 이 부분을 잘 기억하자. 클래스 타입을 참조하는 방법이다.