제네릭은 타입을 불문하고 동작하는 것을 말한다. 즉, 함수에 인수를 넘길 때 타입 인수도 넘기기 때문에 타입에 관한 어떠한 정보도 잃지 않는다. 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
이 부분을 잘 기억하자. 클래스 타입을 참조하는 방법이다.