오늘 마지막 남은 타입스크립트 전체를 정리하고 추가적으로 다음 프로젝트에 넣을 node.js나 react 라이브러리등을 추가로 공부해야겠다.
또 면접 준비를 하며 알게된 CS지식들로 지금 해왔던 프로젝트 들 중 아~ 이런 원리면 이렇게 하지 말걸 이라고 생각한 부분이 되게 많이 드는 것 같다. 다음 프로젝트 땐 반영해보자
타입스크립트에서 항상 사용하며 실무에서도 만날 수 밖에 없는데 그러기에 중요하다
일단 제네릭은 여러 타입에서 사용할 수 있는 내사용 함수나 재사용 클래스를 정의할 수 있게 해 주는 특수 기능 또는 특수 구문 이다
const nums: number[] = [];
const nums: Array<number> = []
이전 배열 타입을 배울 때 이미 사용해본 적이 있었는데 Array<T>
는 특수 타입으로 Array라는 인터페이스임을 알 수 있다
마우스 올려보면 interface Array<T>
라고 입력되어 있는데 여기서 대문자 T
는 타입(Type)을 나타낸다
그래서 Array<number>
이와 같이 T
부분에 타입을 넣으면 숫자로 된 배열 타입을 나타낸다
이러면 이제 모든 종류의 타입을 허용하는 포괄적인 방식으로 작성되고 여기에 타입이 작성되면 기반으로 해서 새로운 타입을 반환한다
const inputEl = document.querySelector("#username")
이 메서드는 타입(Type)을 받는다 querySelector
는 메서드이고 inputEl
는 Element
와 null
이라는 타입을 가지고 있게 된다 (물론 타입스크립트가 스스로 추측한다)
여기서 Element
는 반환받을 수 있는 가장 기본적인 DOM 요소이자 가장 기본적인 객체 타입이다.
우리는 이 엘리먼트가 input엘리먼트라는 걸 알고 있기에 추가해줄수 있다
const inputEl = document.querySelector<HTMLInputElement>("#username")
이경우 inputEl.value
도 작성되고 다 원활한 것을 알 수 있다.
function numberIdentity(item:number):number{
return item
}
function stringIdentity(item:string):string{
return item
}
function booleanIdentity(item:boolean):boolean{
return item
}
이 방식은 단점이 많은데 매번 다른 이름으로 각각의 함수를 만들어줘야 하고 작성할 수 있는 타입의 범위도 제한된다.
문자열 배열 타입도 없고 object 타입도 없다
이런 기능을 하는 함수를 모든 타입별로 일일이 만드는 건 매우 비효율적이다.
일반화 되고 모든 타입에서 작동하는 함수를 작성하는 방법이 있는데
function identity<T>(item: T):T {
return item
}
이 경우 identity
라고 하는 제네릭 함수이며 Type
타입을 입력할 수 있다
indentity<string>
처럼 말이다.
위 처럼 적을경우 위의 제네릭 타입을 가진 identity
함수의 item
은 string 타입을 가질 것이고 반환하는 타입 역시 string 타입을 가지게 된다
뭐든지 입력하고 반환하겠다는 any
와는 확실하게 다른 함수이다.
입력 타입에 따라 그 타입으로 반환한다는 관계를 설정한 것이다
<Type>
은 '모종의'타입이라는의미의 제네릭 버전이다.
물론 <Type>
은 <T>
로 자주사용하는데 참조만 제대로 한다면 마음대로 바꿔 불러도 괜찮다
다른 예시를 살펴보자
function getRandomElement<T>(list:T[]):T{
const randIdx = Math.floor(Math.random() * list.length)
return list[randIdx]
}
getRandomElement<string>(["a","b","c","d"])
getRandomElement<number>([1,34,2,321,54,84,99])
이 경우 함수를 호출할때 어떠한 타입들을 가진 배열을 인자로 보낼건데 제네릭 타입을 통해서 목록의 타입이 어떤지 확인할 수 있고 배열만 받을 것이기에 T
옆에 []
에너테이션을 묶어준다 이후 반환 값도 T
를 이용해 타입을 지정에 반환한다
제네릭 타입의 특징으로 대부분의 경우 typscript는 타입을 추론할 수 있는데
위에서 사용했던 리스트의 랜덤 값을 보여주는 코드에서
function getRandomElement<T>(list:T[]):T{
const randIdx = Math.floor(Math.random() * list.length)
return list[randIdx]
}
getRandomElement(["a","b","c","d"])
getRandomElement([1,34,2,321,54,84,99])
굳이 제네릭 함수에 타입을 선언할 필요가 없다.
타입을 선언하지 않아도 typsecript는 해당 파라미터 혹은 인수를 통해 어떤 타입의 배열이란 걸 충분히 추론할 수 있기 때문이다
하지만 모든 제네릭 함수에 적용되는 것은 아니다! 타입 파라미터를 전달해야 하는 경우도 많은데 당연히 getElementById
나 querySelector
같은 함수는 타입을 추론할 수가 없다..
만약 document.querySelector
가 어떠한 클래스에서 값을 가져오도록 명령할 때 typescript는 다른 파일에 어떤 요소를 가지고 있는지 전혀 모르기 때문이다
이전부터 얘기한 타입스크립트가 언제 동작하는지를 알고 있다면 이해하기 쉽다 (런타임이 아닌 사전에 동작하기 때문)
React와 TSX파일로 작업하게되면 제네릭에서 사용하는 홑화살 구문을 계속 사용하게 될 것이다.
<>
로 jsx와 화살표 함수들을 작성하게 되니까 헷갈릴 수도 있다.
const getRandomElement = <T,>(list:T[]):T =>{
const randIdx = Math.floor(Math.random() * list.length)
return list[randIdx]
}
getRandomElement(["a","b","c","d"])
getRandomElement([1,34,2,321,54,84,99])
후행 쉼표,
를 붙여주면 되는데 TSX파일로 작업하면서 제네릭 함수인 화살표 함수를 작성할 땐 꼭 후행 쉼표를 붙여주자 그렇지 않으면 오류가 발생한다.
function merge(object1, object2){
return {
...object1,
...object2
}
}
merge({name:"colt"}, {pets: ["blue","elton"]})
가령 이 코드들을 변수로 저장하면 어떤 타입이 될까?
const mergeObj = merge({name:"colt"}, {pets: ["blue","elton"]})
경우 mergeObj
의 타입은 any
가 나오게 된다
function merge<T,U>(object1:T, object2:U){
return {
...object1,
...object2
}
}
merge({name:"colt"}, {pets: ["blue","elton"]})
제네릭 타입에 후행쉼표를 추가하여 다른 타입 이름을 써주면 되는데 관행적으로 T,U,V
이런식으로 나가게 된다. for
문에서 i,j,k
이런식으로 나가는 것과 마찬가지
이 경우 반환 타입은 타입스크립트가 추론하여 T & U
라는 교차 타입이라는 걸 확인하게 된다.
function merge<T,U>(object1:T, object2:U){
return {
...object1,
...object2
}
}
merge({name:"colt"}, {pets: ["blue","elton"]})
merge({name:"colt"}, 9) // 오류가 발생하지 않고 실행됨
현재 제네릭은 2가지 타입을 받아오는데 어느 타입의 값이든 가능한 두 객체이다
그렇기에 숫자도 받아졌는데 우리는 이 경우에서 객체 타입만 받도록 하고 싶다.
이럴 땐 extends
키워드를 활용하면 된다
function merge<T extends object,U extends object>(object1:T, object2:U){
return {
...object1,
...object2
}
}
merge({name:"colt"}, {pets: ["blue","elton"]})
merge({name:"colt"}, 9) // 오류가 발생하지 않고 실행됨
넣게되면 아무 타입이나 T타입이 될 수 있는게 아니라 객체 타입으로 확장하는 것이다.
이러면 T
와 U
는 항상 객체여야 한다. 객체라면 어느 타입이든 가능하지만 뭐 문자열로만 이루어져 있거나 숫자등의 다른 값들은 들어오지 않는다
그럼 현재는 객체의 타입은 상관없이 객체이기만 하면 된다고 한 상태인데
object
와 같은 내장 타입으로 확장하지 않고 인터페이스로 확장해보자
interface Lengthy {
length: number;
}
function printDoubleLength<T extends Lengthy>(thing: T):number{
return thing.length * 2
}
좋은 예시는 아니지만
기존 제네릭의 T
만 입력했다면 return
문의 length
는 에러가 발생햇는데 Lengthy
의 interface
로 확장시켜주니 에러가 사라졌다
해당 값을 얻으려면 어떤 타입이든 간에 Lengthy
인터페이스가 지정한 규칙을 따라야 하기 때문이다.
즉, 숫자 타입인 length
프로퍼티가 있어야만 한다
타입 파라미터로 기본값을 갖는 제네릭 함수를 만들 수도 있다
function makeEmptyArray<T>(): T[] {
return []
}
const strings = makeEmptyArray<string>();
const strings2 = makeEmptyArray(); // strings2 : unknown[]
위처럼 만약에 제네릭 타입의 값을 넣어주지 않으면 해당 배열은 unknown[]
이라는 타입을 가지게 된다.
그렇기에 원하는 기본값을 넣고 싶다면
function makeEmptyArray<T = string>(): T[] {
return []
}
const strings2 = makeEmptyArray(); // strings2 : string[]
=
기호를 통해 기본 값을 할당하면 제네릭 타입을 적지 않을 경우 기본 타입을 반환하게 된다
개념은 완전히 똑같다.
interface Song{
title:string;
artist: string;
}
interface Video{
title: string;
creator: string;
resolution: string;
}
class Playlist<T> {
public queue = T[] = [];
add(el: T){
this.queue.push(el)
}
}
const songs = new Playlist<Song>()
songs.add({
title: "제목",
artist: "가수"
})
const videos = new Playlist<Video>()
videos.add({
title: "제목",
creator: "만든사람",
resolution: "해상도"
})
타입이 지정된 제네릭 클래스가 생겼는데 Song
타입의 재생목록과 Video
타입의 재생목록을 만들 수 있다
이처럼 제네릭을 이용해 클래스에 포함되는 다양한 메서드를 작성할 때 타입을 갖도록 구성할 수 있다