typescript 공부중에 generic을 사용하면서 이해가 어려웠던 부분들과 문제를 해결하기 위해서 내용을 기록한다.
아주 간단한 함수 작성을 예로들어 문제를 해결하는 과정을 남겨본다.
우선 제네릭에 대한 정의와 사용에 대한 공식문서 링크를 참고하기 위해 아래 링크를 작성해 놓고 시작해보자
typescript generic official document link 접속!
// 문제1. mix(arr,arr) : 두개의 배열을 매개변수로 받아, 매개변수로 받은 두 배열을 하나의 배열로 섞어서 하나의 배열로 반환합니다.
type Mix = <T,V>(a:T[], b:V[])=>(T|V)[]
const mix:Mix=(arr1,arr2) =>{
return[...arr1, ...arr2]
}
// const result=mix([1,2,3,"a",false,[1]],[0,true,"A"])
// console.log(result)
위 코드블럭에서는 제네릭의 타입을 두개의 매개변수로 받아서 작성한 코드이다.
여기서 의문점은 제네릭 매개변수를 하나만type Mix=<T>(a:T[],b:T[])=>T[]
로 작성해도 코드를 작성하는데 문제가 없다는것이다.
이게 가능한 이유는 mix라는 매개변수 안에 같은 타입의제네릭이 사용되기 때문이다. 매개변수에서 다른 타입을 사용한다면 Mix의 제네릭 타입도 두개가 필요하다는 것이다.
type Mix = <T>(a:T[], b:T[])=>T[] -> 제네릭의 매개변수를 하나만 사용
const mix:Mix=(arr1,arr2) =>{
return[...arr1, ...arr2]
}
const result=mix([1,2,3,false,[1]],["A"]) -> 제네릭 매개변수를 하나만 사용했지만 mix 함수의 두번째 매개변수에는 첫번째 매개변수에 없었던 string type이 들어가 있기 때문에 'error'가 발생한다.
// console.log(result) -> typescript의 영향인거지 javascript에서는 console.log는 찍힘
// 문제2. count(arr) : 배열을 매개변수로 받아, 매개변수로 받아온 배열의 길이를 반환하면됩니다.
type Count = <T>(a:T[]) => number
const count:Count = (arr) => {
return arr.length
}
// const result=count([1,2,3,"a",false,1,[1]])
// console.log(result)
위 코드블럭에서는 generic의 return 타입이 number로 되어야 정상적으로 작동한다. 하지만 type Count = <T>(a:T[]) => T
가 되었을때는 에러를 발생시킨다. 에러 내용은
Type '<T>(arr: T[]) => number' is not assignable to type 'Count'. Type 'number' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'number'
에러내용을 살펴보면 return에 number type이 와야 하는데 T가 와있다는 것이다.
return의 length가 number type이기 때문인데 왜 T가 오면 되지않는 이유는
결국 return하는 반환 타입이 number 이기때문이다. length 메소드는 number 타입의 형태
이기 때문이다.
만약 type Count = <T>(a:T[]) => T
였다면 마지막 반환(=>T)은 제네릭이 string type이었다면 string type으로 반환되는 것이 제네릭의 특성이기 때문이다. 그렇기 때문에 제네릭의 return 에 number를 명시해 준 것이다.
//문제3. findIndex(arr, item) : 첫번째 매개변수로 배열을, 두번째 매개변수로 받아온 item이 첫번째 매개변수 arr배열의 몇번째 index로 존재하는지 체크한후 존재한다면 몇번째 index인지 반환하고 존재하지않는다면 null을 반환합니다.
type FindIndex= <T>(a:T[],b:T)=>number | null
const findIndex:FindIndex=(arr,item)=>{
if(arr.includes(item)){
return arr.indexOf(item)
}else{
return null
}
}
위 코드블럭도 case2
와 비슷한 케이스 인데 type Count = (a:T[]) => T|null`일때 에러가 발생한다.indexOf() 메소드가 number type을 반환하기 때문에 생기는 에러인데 else의 케이스에서 null이 return 되기 때문에 return type에null도 추가로 포함을 시켜줘야 하는지도 의문이었다. T라는 generic이 있을때 반환하게 되면 null도 포함되어있을것이라고 생각했었는데 말이다.
이 이유는 case2 번의 이해와 같다. indexOf의 메소드는 number type return을 한다. 그렇기에 제네릭타입에서도 number를 명시해주는것이고 findIndex의 함수에서 if문에서 indexOf를 반환하지 않을때 null을 반환하기 때문에 null type return도 명시를 해줘야 하는것이다.
//문제 4. slice(arr, startIndex, endIndex): 첫번째 매개변수로 배열 arr을 받고, 두번째 매개변수로 숫자 startIndex, 세번째 매개변수 숫자 endIndex를 받습니다. 첫번째 매개변수 arr을 두번째 매개변수로 받은 startIndex부터 세번째 매개변수로 받은 인덱스까지 자른 결과를 반환하면됩니다. 이때 세번째 매개변수는 필수 매개변수가 아닙니다.
type Slice= <T>(a:T[],b:number,c?:number)=>void
const slice:Slice=(arr,startIndex,endIndex?)=>{
return arr.slice(startIndex, endIndex+1) // ->endIndex에 대한 error 발생. optional이기 때문에 number type | undefined type이 되어야 한다.
}
const result=slice([1,2,3,"a",false,1,[1]],2,3)
console.log(result)
위 코드블럭은 return값이 있는데 generic의 return type에서는 void가 되어야 정상적인 generic type작성이 된다는 것이다. return을하게되면 위 소스코드일때는 배열 형태로 return이 되는데 왜 void가 와야하는지 의문이었다. void는 아무것도 반환하지 않을때 사용하는 type으로 알고 있는데 말이다.
그리고 매개변수 3번째의 내용은 필수가 아닌 optional로 작성했는데도 에러가 났다. 정상적인 코드를 작성하려면 참고해야하는 내용이 있다. 제네릭 타입의 매개변수 c가 optional이기 때문에
optional에 대한 경우를 구분해서 slice 함수를 작성해줘야 한다.
그럼 여기서 에러가 발생하지 않게 옳게 작성한 제네릭타입과 소스코드는
type Slice= <T>(a:T[],b:number,c?:number)=>void
const slice:Slice=(arr,startIndex,endIndex?)=>{
return endIndex === undefined ? arr.slice(startIndex) : arr.slice(startIndex,endIndex)
}
const result=slice([1,2,3,"a",false,1,[1]],2,3)
console.log(result)
삼항연산자로 나타내 보았다. 이렇게 되면 에러없이 코드가 작성된다.
case4의 경우는 return값이 있는데도 제네릭 타입에서 void를 사용했다. 이는 좀더 심화적인 공부가 필요하겠지만
지금 지식의 상태에서는 제네릭의 return type이 void여도 반환을 하는 이유는 함수의 return이 함수를 이용해 return된다면 void를 사용할 수 있다
는 점이다.(일단 이정도로만 이해하고 있자)
코드들을 typescript의 환경으로 브라우저에 작성할때 참고하려면 typescript playground로 접속해서 브라우저 환경에서도 코드작성이 가능하다.
실무로 generic을 사용하는것이 얼마나 많을지는 모르겠지만 기본 원리에 대해서는 공부를 해두고 이를 해결하기 위한 케이스가 발생할 수 있다고 생각하기때문에 이렇게 남겨놓고 활용하는 일이 발생할 때 내가 이런 공부들을 했었고 해결했다는 것에 의의를 두어야 겠다.
그리고 무엇보다 제네릭타입에서도 위의 케이스에서는 결국 타입추론을 사용했지만 타입추론보다는 명확하게 인지 할 수 있도록 제네릭타입도 타입을 명시해주면 베스트인것 같다.