이번에 notion api를 사용하면서 데이터를 가공할 일이 있어서 별도의 함수를 만들다가
외부api에 따른 데이터 타입이 명확하지 않아 계속 Lint가 날 괴롭히는 일이 있었다.
1. Record로 생성된 타입을 타입 스크립트가 몰라..
우선 Notion에 내가 설정한 테이블은 이런 식인데
Name | Gruop | Tags | Create Date | state
이러한 필드 들은 사용자가 필요한 것을 가져다 추가하는 형태였다.
그러다 보니 Notion에서 제공하는 함수의 리턴 타입인 QueryDatabaseResponse가 여러 타입을 유니온 해서 반환하고 있었다.
export type QueryDatabaseResponse = { type: "page_or_database"; page_or_database: EmptyObject; object: "list"; next_cursor: string | null; has_more: boolean; results: Array<PageObjectResponse | PartialPageObjectResponse | PartialDatabaseObjectResponse | DatabaseObjectResponse>;
내가 정의한 필드의 값이 있는 페이지 데이터만 가져오면 돼서 이번에 타입을 좁히고
필요 데이터만 추출 하는 함수를 만들었다.
파라미터는 PageObjectResponse타입의 배열을 받는 것으로 만들었다.
export function normalize(postList:PageObjectResponse[]):PostList{ const reponseData = new Array<Post>; postList.map((item)=>{ reponseData.push(extraData(item)); }) return reponseData; } function extraData(pageData:PageObjectResponse):Post{ return { title:pageData.properties.Name.title[0].plain_text ?? "제목없음", tags : pageData.properties.multi_select.map(tag=>tag.name), state : pageData.properties.state.status.name, group: pageData.properties.group.rich_text[0]?.plain_text ?? "", createDate: pageData.properties["Created Date"].date?.start ?? "", pageId : pageData.id } }
여기서 부터 두통을 울리는 Lint의 타입 스크립트의 거부권을 만났는데
응답 데이터의 properties 하위에 있는 객체들이 있는지 없는 지를 타입 스크립트가 모른다고 하는 것이다.
PageObjectResponse 타입은 내부를 들여다 보면 아주 유연한 타입을 properties 에 두었는데
properties: Record<string, { type: "number"; number: number | null; id: string; } | { type: "url"; url: string | null; id: string; } | { type: "select"; select: PartialSelectResponse | null; id: string; } | { type: "multi_select"; multi_select: Array<PartialSelectResponse>; id: string; } ...
이런 방식으로 Notion DB에 필드명과 값을 자유롭게 받을 수 있게 해둔 것이다.
Record를 오랜만에 봐서 좀 당황하긴 했는데
Record<KeyType, ValueType> 맴버 이름은 keyType, 값은 ValueType으로 타입을 만드는 녀석
이로써 내가 DB를 자유롭게 만들고 그거에 대한 값을 Notion API가 자유롭게 가져올 수 있다는 걸 알았다.
본론으로 돌아가서 이제 그럼 그 값들을 내가 잘 받아서 내가 원하는 형태로 리턴을 해야 하는데….
2. Extract로 타입 떠 먹여주기
우선은 properties 하위의 필드에 내가 DB에 선언한 필드들이 있다는 걸 알려줘야 했다.
그러기 위해서는 Name | Gruop | Tags | Create Date | state 의 키 값을 알려줘야 하고
그 값들이 어떤 형태로 존재하는지도 알려줘야 했다.
여기서 부터는 G선생님의 도움을 받았는데
Extract<T, U>
를 사용해서 타입을 정의하고
사용할 값은 정의한 타입이라고 선언해줘야 해!
바로 NotionApi의 타입을 지정하는 별도의 파일을 만들어서 아래와 같이 선언했다.
import { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints'; export type PropertyValue = PageObjectResponse['properties'][string]; type notionApititle = Extract<PropertyValue,{type:'title'}>; type notionApiTags = Extract<PropertyValue,{type:'multi_select'}>; type notionApiState = Extract<PropertyValue,{type:'status'}>; type notionApiGroup = Extract<PropertyValue,{type:'rich_text'}>; type notionApiDate = Extract<PropertyValue,{type:'date'}>; export type {notionApititle,notionApiTags, notionApiState, notionApiGroup ,notionApiDate};
api요청으로 온 값의 properties 하위에는 내가 DB에 선언한 임의 string 타입의 필드명이 있으니
PageObjectResponse['properties'][string]
타입을 이용해,
{type:'title'} 형태로 어떤 식으로 데이터가 저장되었는지 각각 선언해 주었다.
내가 설정한 DB 필드들이 어떤 타입(
title
, multi_select
, status
, ...)으로 응답되는지를 기반으로
각각 Extract
를 통해 원하는 타입만 뽑아낼 수 있었다.
그 다음 함수의 호출 부분에서 이제 다시 재정의 해주면 된다!
function extraData(pageData:PageObjectResponse):Post{ const name = pageData.properties['Name'] as notionApititle; const tags = pageData.properties['Tags'] as notionApiTags; const state = pageData.properties['state'] as notionApiState; const group = pageData.properties['group'] as notionApiGroup; const createdDate = pageData.properties['Created Date'] as notionApiDate; return { title: name.title[0]?.plain_text, tags: tags.multi_select.map((tag) => tag.name), state: state.status.name, group: group.rich_text[0].plain_text, createDate: createdDate.date.start, pageId: pageData.id, } }
여기서 끝나면 정말 깔끔했는데.. 문제가 또 생겼다…
3. Notion api중 null을 예상한 자료들…
state와 cretateDate는 properites에 선언된 부분을 보면 null을 유니온 타입으로 가지고 있다.
api 내용을 보면 리턴 타입이 배열 기반인 녀석들은 빈 배열로 넘어오는데
하나의 값으로 넘어오는 녀석들은 없을 때를 대비해서 null을 포함한 것 같았다.
그래서 옵셔널 채이닝을 추가해 줬다.
title: name.title[0]?.plain_text, tags: tags.multi_select.map((tag) => tag.name), state: state.status?.name??"시작 전", group: group.rich_text[0].plain_text, createDate: createdDate.date?.start ?? '', pageId: pageData.id,
드디어 끝!