Generic은 타입을 정하지 않고 여러 타입을 사용할 수 있게 해준다.
즉, 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다. 한번의 선언으로 다양한 타입에 '재사용'이 가능하다는 장점이 있다.
제네릭을 쓰지 않을 경우, 불필요한 타입 변환을 하기 때문에 프로그램의 성능에 악영향을 미치기도 하는데, 제네릭을 사용하게되면 따로 타입 변환을 할 필요가 없어서 프로그램의 성능이 향상되는 장점이 있다.
Typescript의 Generic 문법은 함수와 클래스, 인터페이스에서 사용할 수 있다.
<> 괄호를 이용하여 선언할 수 있다. <>안에 선언된 T는 일종의 매개변수처럼 타입 정보를 가지고 있으며 이 타입 정보로 매개변수의 타입을 결정할 수 있다.
<>안에는 어떠한 단어도 들어올 수 있다.
알 수 없었던 첫번째 매개변수의 타입이 Generic 문법을 통해서 타입이 정해지게 된다. 아래 예시처럼 toArray('neo', 123)
의 첫번째 인자는 string이고 두번째 인자는 number인데 오류가 발생하는걸 볼 수 있다.
그 이유는 첫번째 인자의 타입이 string이기 때문에 Generic 문법을 통해서 타입 추론을 하게되어 function toArray<T>(a:T, b:T)
가 function toArray(a:string, b:string)
으로 되었다고 볼 수 있다.
interface Obj{
x:number
}
type Arr = [number, number]
// <> 괄호를 이용하여 선언
// <>안에 선언된 T는 일종의 매개변수처럼 타입 정보를 가지고 있다.
// 이 타입 정보로 매개변수의 타입을 결정할 수 있다.
function toArray<T>(a:T, b:T){
return [a, b]
}
console.log(
toArray('neo', 'anderson'),
toArray(1,2),
toArray(true,false),
toArray({x:1},{x:2}),
toArray([1,2],[3,4]),
toArray('neo', 123),// 'number' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없습니다
// 또는 이렇게 명시적으로 T의 타입을 지정하는 방법도 있다.
toArray<string>('kim','park')
)
class에선 아래와 같이 사용할 수 있다.
class User<P>{
public payload: P
// payload의 타입을 지정해 주지 않으면
// 'payload' 매개 변수에는 암시적으로 'any' 형식이 포함됩니다. 오류 발생
// 클래스에 Generic을 사용하여 payload의 타입을 지정해 줄 수 있다.
constructor(payload : P){
this.payload = payload
}
getPayload(){
return this.payload
}
}
위의 예제를 좀 더 축약하면 아래와 같이 작성할 수 있다.
class User<P>{
constructor(public payload : P){
}
getPayload(){
return this.payload
}
}
class를 생성자 함수를 통해 호출할 때에는 아래와 같이 사용할 수 있다.
<>를 생성자 함수를 호출할 때 사용하면 된다. 이렇게 되면 UserAType이라는 인터페이스의 내용이 <P>
에 들어가게 되고 <P>
의 타입이 UserAType이 된다.
만약 User라는 이름의 하나의 class를 사용할 때마다 다른 타입의 데이터를 만들고 싶다면 Generic을 사용해 타입을 필요에 따라 유연하게 사용할 수 있다.
class User<P>{
constructor(public payload : P){
}
getPayload(){
return this.payload
}
}
interface UserAType{
name:string
age:number
isValid:boolean
}
interface UserBType{
name:string
age:number
emails:string[]
}
const user = new User<UserAType>({
name:'kim',
age:23,
isValid:true,
emails:[] // 개체 리터럴은 알려진 속성만 지정할 수 있으며 'UserAType' 형식에 'emails'이(가) 없습니다
})
마찬가지로 이름 뒤쪽에 선언할 수 있다. 아래와 같이 const dataA:MyData<string>
부분에 선언된 것 처럼 string으로 명시되어 있다면 value의 값은 string으로, number라면 number로 들어가야 한다.
interface MyData<T>{
name:string
value:T // 이 값의 타입은 T의 타입을 따라가게 된다.
}
const dataA:MyData<string> = {
name:'Data A',
value:123 // 'number' 형식은 'string' 형식에 할당할 수 없습니다.
}
const dataB:MyData<number> = {
name:'Data B',
value:1
}
const dataC:MyData<boolean> = {
name:'Data C',
value:true
}
const dataD:MyData<number []> = {
name: 'Data D',
value: [1,2,3,4]
}
인터페이스의 제네릭에 조건을 달고 싶다면 extends 키워드를 사용하면 된다.
아래와 같이 사용하게 되면 T의 타입은 string 이거나 number 타입만 허용한다는 의미가 된다.
interface MyData<T extends string | number>{
name:string
value:T
}
const dataA:MyData<string> = {
name:'Data A',
value:'Hello World'
}
const dataB:MyData<number> = {
name:'Data B',
value:1
}
const dataC:MyData<boolean> = { // 'boolean' 형식이 'string | number' 제약 조건을 만족하지 않습니다
name:'Data C',
value:true
}
const dataD:MyData<number []> = { // 'number[]' 형식이 'string | number' 제약 조건을 만족하지 않습니다
name: 'Data D',
value: [1,2,3,4]
}