제네릭은 타입 정보를 동적으로 결정하며 유연성을 제공하는 타입을 말한다.
만약 인자로 받은 값을 반환하는 함수가 있다고 가정할 때, 모든 number, string, boolean 등 모든 타입을 다 받아야 할 때 제네릭을 이용하면 된다.
function func<T>(value: T): T {
return value
}
let num = func(10) // number
let str = func("Hello") // string
let bool = func(true) // boolean
함수 이름 뒤에 <>를 열고 타입을 담는 변수인 타입 변수 T를 선언한다.
그리고 매개변수와 반환값의 타입을 타입변수 T로 설정하면 된다.
그러면 number타입의 value를 받으면 타입 T는 number로 추론되고, string타입의 value를 받으면 타입 T는 string으로 추론된다.
또한 제네릭은 호출할 때 타입변수에 할당할 타입을 직접 명시해도 된다.
function func<T>(value: T): T {
return value
}
let arr = func<[number, number, number]>([1, 2, 3])
<>안에 튜플 [number, number, number]으로 타입을 지정했다.
매개변수 a, b로 두 개의 값을 받아 위치를 swap하는 함수를 만들려고 한다.
그러면 a와 b의 타입을 지정해야 한다.
제네릭을 사용해서 구현하고자 한다면
function swap<T>(a: T, b: T) {
return [b, a]
}
이렇게 구현할 수 있지만, 만약 a와 b의 타입이 서로 다르면 위 함수는 사용할 수 없다.
그럴 때는 타입 변수를 두 개를 사용하면 된다.
function swap<T, U>(a: T, b: U) {
return [b, a]
}
<>안에 타입 변수 T와 U를 사용해 각각의 매개변수에 타입을 지정해줌으로써 각각의 타입을 따로 받을 수 있게 된다.
인터페이스에도 제네릭을 적용할 수 있다.
interface KeyPair<K, V> {
key: K
value: V
}
let keyPair: KeyPair<string, string> = {
key: "key",
value: "value",
}
let keyPair2: KeyPair<string, number[]> = {
key: "key",
value: [1, 2, 3],
}
key 타입은 타입변수 K, value 타입은 타입변수 V로 제네릭을 이용해서 인터페이스를 구성했다.
제네릭 인터페이스를 인덱스 시그니쳐와 함께 사용하면 더 유연한 객체 타입을 정의할 수 있다.
interface Map<V> {
[key: string]: V
}
let stringMap: Map<string> = {
key: "value"
}
let numberMapL: Map<number> = {
key: 10
}
Map<V> 인터페이스는 인덱스 시그니쳐로 key의 타입은 string, value의 타입은 V인 모든 객체 타입을 정의한다.
제네릭 타입 별칭도 인터페이스와 마찬가지로 동일하게 적용할 수 있다.
type Map2<V> = {
[key: string]: V
}
let boolMap: Map<boolean> = {
key: true
}
interface Student {
type: "student";
school: string;
}
interface Developer {
type: "developer";
skill: string;
}
interface User {
name: string;
profile: Student | Developer;
}
function goToSchool(user: User<Student>) {
if (user.profile.type !== "student") {
console.log("잘 못 오셨습니다.")
return
}
console.log(`${user.profile.school}로 등교 완료.`)
}
profile로 Student와 Developer 인터페이스를 가지는 User 인터페이스가 있다.
goToSchool()는 User<Student> 타입의 user를 받을 때만 실행하는 함수로 타입좁히기를 통해 기능을 수행하고 있다.
이러한 상황에서 제네릭 인터페이스를 사용하면 타입좁히기 없이 goToSchool()를 구현할 수 있다.
interface Student {
type: "student";
school: string;
}
interface Developer {
type: "developer";
skill: string;
}
interface User<T> {
name: string;
profile: T;
}
function goToSchool(user: User<Student>) {
console.log(`${user.profile.school} 등교`);
}
User<T>인터페이스를 통해 타입변수 T로 Student를 받으면 Developer 타입이 올 수 없게되므로 타입좁히기 없이 간결하게 함수를 구현할 수 있게 된다.
클래스에도 마찬가지로 제네릭을 적용하면 유연한 클래스가 될 수 있다.
class List<T> {
constructor(private list: T[]){}
push(data: T) {
this.list.push(data)
}
pop() {
return this.list.pop()
}
print() {
console.log(this.list)
}
}
const numberList = new List([1, 2, 3])
const stringList = new List(["a", "b", "c"])
만약 제네릭을 이용하지 않고 List클래스를 구현하려고 하면
string타입의 stringList, number타입의 numberList ... 이런식으로 모든 타입에 해당하는 클래스를 각각 구현해야 하는 번거로움이 발생하게 된다.
promise도 제네릭 클래스로 구현되어 있기에 promise를 생성할 때에도 타입 변수에 할당할 타입을 직접 설정해줘야 한다.
const promise = new Promise<number>((resolve, reject) => {
setTimeout(() => {
resolve(20)
}, 3000)
})
promise.then(res => console.log(res * 10) // res: number 타입
만약에 제네릭을 사용하지 않고 promise를 생성했다면 res는 unknown타입으로 추론되기에 res * 10 같은 연산이 불가능하게 된다.
만약 함수의 반환값이 Promise 객체면 반환 타입을 Promise<타입>으로 명시해줘야 한다.
interface Post {
id: number
title: string
content: string
}
function fetchPost() {
return new Promise<Post>((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
title: "new title",
content: "new content",
})
}, 3000)
})
}
또는 직접 반환값 타입을 작성해도 된다.
function fetchPost(): Promise<Post> {
...
}