Total Typescript - advanced type transformation

김동하·2025년 4월 15일
0

typescript

목록 보기
16/21
post-thumbnail

1. string to object

자바스크립트에서 쿼리 스트링을 쿼리 파라미터로 만드는 것처럼 타입스크립트로 string을 obeject로 변환이 가능하다.

몇 시간 도전했지만 실패한 그 문제를 함께 알아보자

문제

흔히 볼 수 있는 url 비슷한 string이 있다. 여기서 우리는 새로운 객체를 만들고 싶은 욕구를 참을 수 없어진다.

/users/:id 라면 { id : string }으로
/users/:id/organisations/:organisationId라면 { id: string; organisationId: string }로 변환하고 싶다.

그렇게 하여 아래 테스트 코드를 통과하는 것이 목표다. 그럼 한번 도전해보자

Split

먼저 저 string을 쪼개야할 것 같은 느낌이 온다. string을 어떻게 split하지...?

사실 매우 좋은 패키지가 있다. 바로 ts-toolbelt !

말도 안되는 타입 유틸을 많이 제공한다.

그중에서도 Split을 사용할 건데 패키지의 내부 구현 코드는 복잡하여 내가 이해한 바를 ChatGPT의 도움을 받아 작성해보면

type Split<
  S extends string,
  D extends string = ""
> = S extends `${infer Part}${D}${infer Rest}`
  ? Part extends "" 
    ? Split<Rest, D>       // 빈 부분이면 무시
    : [Part, ...Split<Rest, D>] // Part를 배열에 추가
  : S extends "" 
    ? []                   // 남은 문자열이 빈 값이면 종료
    : [S];                 // 마지막 부분 추가

핵심은 Part에 값이 있냐를 보고 분기를 타서 재귀적으로 Split을 수행하는 것이다.

Split은 여기까지 하고 다시 문제로 돌아가면

Split은 string과 split해줄 구분자를 넣어주면 된다.

이런 식으로 작성할 수 있을 것이다. 그럼 엄청난 에러와 마주하게 된다.

이유인 즉슨 Split<T, "/">에서의 반환값은 튜플이다.

type UserPath = "/users/:id";

type Splited = Split<UserPath, "/"> // ["users", ":id"]

순회하기 위해서 number 키워드가 필요하다

원하는 형태가 조금씩 나오고 있다!

infer

이제 as로 재맵핑과 infer가 필요하다. :가 붙은 형태라면 K를 키로써 남기고 그게 아니라면 never 하면된다.

K를 infer로 추론하는 부분까지 추가해주면 완성이다.

테스트는 모두 통과!

2. object to discriminated union

자, 다음 문제는 객체를 구별된 유니언으로 변경하는 것이다. 간단해보이는데 어렵다...

MutuallyExclusive 타입을 완성시켜서 '

테스트를 통과하면 된다. 그럼 하나씩 해결해보자

먼저, 키 맵핑형태로 만들어준다.

이제 생성된 객체들을 유니온으로 묶기 위해 인덱스 접근을 한다.

여기서 살짝 난관에 부딪혔었는데, 추론 타입이 다시 string이 되어버려 무언가 잘못했나 싶었다. 하지만 우리는 재맵핑하는 과정에 있으므로 string이 맞는 추론이다.

만약 T[K]K로 변경해보면

유니언으로 잘 추론하고 있다. KRecord 로 키 밸류 쌍으로 만들자.

멋진 추론

테스트는 모두 통과다.

3. discriminated union to object

타입스크립특 공부만하면 왜 이렇게 피곤해지는지 모르겠다.. 아무튼 이제 반대로 구별된 유니언을 객체로 변경하는 문제다.

이렇게 생긴 Route 유니언이 있다. 각 객체는 route, search? 키를 가진다.

Route 유니언을 객체 타입으로 만들 것이다. 자 그럼 들어가보자.

뭘 먼저 해야하지 헷갈릴 땐 일단 순회를 돌아보자. 유니언 타입들이므로 indexed해서 각 키를 순회해보면 될 것 같다.

원하는 형태가 조금 보이기 시작한다. 이제 임의로 지정해둔 stringRoute["search] 느낌으로다가 변경하면 될 거 같다.

하지만 여기서 살짝 문제가 발생하는데, search가 옵셔널이기 때문에 속성이 없다는 컴파일 에러가 나온다

이때 infer가 등장한다. 그렇기 위해선 먼저[R in Route["route"]]를 수정해줘야 한다.

Route["route"]는 각 유니온 ("/" | "/about" | "/admin" | "/admin/users")을 순회하여 문자열 리터럴로 만든다.

여기서 생성된 R은 단순 문자열이므로 원본 유니온의 프로퍼티에 접근할 수 없다. Route 유니온의 각 멤버 객체에 접근하는 R을 생성해야 한다.

이렇게 유니언 멤버에 접근하는 R을 만들고 이제 infer해보자.

R extends { search : any} ? R["search"] : never에서 search 객체를 infer해서 조건부로 명시해주면 끝이다.

(휴.. 너무 어렵고만.. )

당당하게 테스트도 통과했다.

4. deep partial

마지막으로 deep partial 유틸에 대해 살펴보려고 한다. 오늘은 너무 피곤하니 여기까지만 하고 끝내야겠다.

deep partial은 말그래도 내부 객체까지 모두 파셜하게 만드는 유틸함수다.

요렇게 내부 객체의 모든 프로퍼티가 옵셔널하게 변경되면 테스트 통과다.

recusive

무엇을 먼저 해야할지 고민이 된다면 일단 순회를 때리자

가장 바깥 뎁스에 있는 프로퍼티만 옵셔널에 걸린 걸 확인할 수 있다. 그렇다면 저 순회하면서 옵셔널을 만드는 행위를 재귀로 하면 될 듯 해보인다.

설마 이게 끝일까 했는데

테스트에 통과하지 않는다.

그런데 아무리 생각해도 이상하다. 실제 DeepPartial을 적용해보면 제대로 옵셔널로 추론하고 있다.

array

이유인즉슨, 배열때문이다! g가 배열이기 때문에 배열 자체에 옵셔널을 먹여버리는 것!

하나씩 떼어서 살펴보면

우리가 궁극적으로 도달하고 싶은 형태는 각 프로퍼티에 대한 옵셔널이다.

하지만 타입이 배열인 경우 조금 다르게 옵셔널이 되어 있는 것을 볼 수 있다.

undefined를 옵셔널로 허용해버리는 이상한 타입이 되어버린 것이다.

즉, 리커시브를 할 때 대상이 배열일 경우는 다르게 해줘야한다!
무언가 느낌적인 느낌으로 infer를 사용해야함을 느낀다

infer

먼저 Array를 발라줄 조건부를 추가한다. 그리고 any 대신 infer를 추가한다.

휴.. 머리가 지끈거린다.. 여기서 우리가 원하는 것은 배열 각 요소에 대한 DeepPartial이다. 그래서 T가 배열인 경우, Array<infer U>를 통해 배열 요소인 객체를 infer했다.

그리고 Array<DeepPartial<U>> 그 객체(U)를 DeepPartial하고 다시 Array로 만들어줬다.

이제 배열G가 원하는대로 옵셔널한 형태를 갖는다.

undefined도 불가하다.

당연히 테스트도 모두 통과다.

(하 오늘 타입스크리트 여기까지)

참고: total-typescript

profile
프론트엔드 개발

0개의 댓글