코딩/Typescript

타입스크립트 keyof 연산자 - 타입스크립트 TypeScript 강좌 8편

드리프트 2021. 11. 8. 18:09
728x170

 

 

 

안녕하세요?

 

지난 시간에 이어 타입스크립트 제네릭에 있어 keyof 연산자를 사용하는 방법에 대해 알아보겠습니다.

 

우리가 객체에서 특정 항목을 기준으로 객체의 값을 뽑아주는 함수를 만들려고 한다고 가정해 봅시다.

 

const squid = [
    {
        name: "Ki-Hoon",
        id: 456,
    },
    {
        name: "Sae-Byuk",
        id: 67
    }
]

 

자, 위와 같은 객체의 배열이 있다고 합시다.

 

여기서 객체의 항목 중에 name과 id가 있는데, 이 항목 중에 한 개만 골라서 출력하는 함수를 만들어 보겠습니다.

 

function extract(items: unknown[], key: string) : unknown[] {
    
}

 

함수의 형태를 unknown 타입으로 만들어 보았는데요.

 

items라는 배열과 key 항목이 있습니다.

 

이제 이걸 제네릭으로 바꿔 볼까요?

 

function extract<DataType, KeyType>(items: DataType[], key: KeyType)
 : DataType[KeyType][] {

}

 

일단 제네릭 부분을 DataType과 KeyType 두 개의 템플릿을 넣었습니다.

 

여기서 KeyType은 DataType의 항목 중 하나입니다.

 

왜냐하면 우리가 만들 함수는 DataType이라는 객체에서 특정 항목만 추출하는 함수 이거든요.

 

그래서 이럴 때 쓰이는 게 바로 keyof 연산자입니다.

 

다음과 같이 쓰면 됩니다.

 

function extract<DataType, KeyType extends keyof DataType>(
    items: DataType[], key: KeyType
): DataType[KeyType][] {

}

KeyType extends keyof DataType이라고 표현했습니다.

 

즉, KeyType은 DataType의 키 중에 하나라는 표현입니다.

 

최종 코드입니다.

 

function extract<DataType, KeyType extends keyof DataType>(
    items: DataType[], key: KeyType
): DataType[KeyType][] {
    return items.map(item => item[key])
}

이제 실행해 볼까요?

 

 

 

extract 제네릭 타입이 아직까지는 unknown, never라고 나옵니다.

 

이제 위에서 만든 객체를 넣어 볼까요?

 

 

위 그림에서 보듯이 items는 squid 타입처럼 name과 id가 있고, key 부분은 "name" | "id"라고 아주 명확하게 명시되어 있습니다.

 

즉, key는 "name" 일수 있고 "id" 일 수 있다는 얘기입니다.

 

그리고 리턴 타입도 string과 number 둘 중에 하나로 나옵니다.

 

제네릭이 잘 작동한다는 뜻이죠.

 

""를 넣으니까 바로 VS Code 가 알려줍니다.

 

넣을 수 있는 게 두 개라고요.

 

 

"name"을 넣었습니다.

 

바로 위와 같이 실행 결과는 squid 객체 배열에서 "name" 부분만 추출했네요.

 

성공했습니다.

 

이렇듯 keyof 연산자는 특정 제네릭을 특정하게 지정할 수 있는 아주 고마운 역할을 합니다.

 

이제 keyof 연산자를 이용한 좀 어려운 예제를 들어 볼까요?

 

다음과 같이 Event 관련 인터페이스를 정의해 보겠습니다.

 

interface BaseEvent {
    time: number;
    user: string;
}

interface EventMap {
    addToCart: BaseEvent & { quantity: number; productID: string; },
    checkout: BaseEvent
}

 

BaseEvent 인터페이스가 있고 그리고 EventMap에는 Event가 있는데 첫 번째 Event인 addToCart는 BaseEvent에 { quantity: number; productID: string; } 이 추가된 형태입니다.

 

그리고 checkout Event는 그냥 BaseEvent인 경우죠.

 

이제 이런 인터페이스가 있다고 했을 때 sendEvent 함수를 제네릭으로 만들어 볼까요?

 

function sendEvent(name: string, data: unknwon): void {

}

 

위와 같이 일단 형태를 만들었습니다.

 

그럼 여기서 어떻게 바꿔야 제네릭 타입이 될까요?

 

function sendEvent<Name>(name: Name, data: unknwon): void {

}

 

일단 name 부분을 Name이란 제네릭으로 만들었습니다.

 

그럼 data 부분인데요.

 

data는 EventMap 중에 하나이니까 다음과 같이 만들면 됩니다.

 

function sendEvent<Name>(name: Name, data: EventMap[Name]): void {

}

뭔가가 완성된 거 같은데요.

 

여기서 Name은 EventMap의 하나이기 때문에 그 부분을 추가해 보도록 하겠습니다.

 

function sendEvent<Name extends keyof EventMap>
  (name: Name, data: EventMap[Name])
  : void {
  console.log([name, data]);
}

 

역시 extends keyof 연산자를 썼는데요.

 

Name은 EventMap의 하나라는 뜻입니다.

 

이제 sendEvent 함수를 테스트해 볼까요?

 

 

sendEvent라고 쓰니까 VS Code가 친절하게 알려주네요.

 

name은 EventMap 중에 하나라고요.

 

이제 ""를 입력해 볼까요?

 

 

""를 입력하니까 addToCart와 checkout이 나옵니다.

 

예상대로 우리가 지정한 EventMap 중에 하나이기 때문입니다.

 

 

코드를 마저 이어 보면 그 뒤쪽 data 부분도 타입이 정확히 나오고 있습니다.

 

 

 

{}를 입력해 보면 역시 넣을 수 있는 항목이 잘 나오고 있습니다.

 

sendEvent("addToCart", { time: 0, user: "Sung", quantity: 1, productID: "Squid" })

 

 

실행 결과도 잘 나오고 있네요.

 

지금까지 타입스크립트 제네릭에서 keyof 연산자에 대해 알아보았습니다.

 

 

그리드형