재귀로 객체 내 모든 키값을 추출하는 타입 만들기
객체 내 모든 키를 추출할 수 있는 타입을 만든 후, 컴포넌트에 적용하여 컴포넌트가 props로 받는 객체의 타입을 스스로 추론하게 만들어보자.
객체 재귀 타입(PropertyPath) 구현
type PropertyPath<
T extends Record<string, unknown>,
U extends 'recursive'[] = [],
> = U['length'] extends 10
? never
: {
[K in keyof T & string]: T[K] extends Record<string, unknown>
? K | `${K}.${PropertyPath<T[K], [...U, 'recursive']>}`
: K
}[keyof T & string]
위의 PropertyPath 타입은 객체의 모든 키를 추출하는 타입이다. 원활한 이해를 위해 몇가지 사전 지식이 필요하다.
- keyof 연산자
- 조건부 타입
- Record<string , something>
- unknown 타입
각각에 대한 설명은 타입스크립트 핸드북을 찾아보면 잘 설명되어 있다. 모르는게 있을 때 참고하자.
https://www.typescriptlang.org/
구현 원리는 아래와 같다.
- 인자로 넘겨받는 제네릭(T)이 객체인지 아닌지 판별한다.
- U의 length를 판단한 뒤 10이라면 아무것도 반환하지 않고 재귀를 종료한다.
- length가 10이 아니라면 객체의 키에 해당하는 값을 확인한다. 값이 중첩된 객체라면 재귀를 돌린다.
- 중첩된 객체가 아니라면 객체의 키값을 반환하고 재귀를 종료한다.
- 중첩된 객체라면 객체의 중첩이 없을때까지, 또는 U의 length가 10을 초과할때까지 재귀를 돌린다.
- ... 반복
복잡해보이지만 이해하면 그렇게 어렵지만은 않다.
재귀 시 [keyof T & string] 와 같은 특이한 타입이 있는데, 이와 같이 코드한 이유는 unknown이 뿜는 에러를 막아주기 위해서다. unknown은 알수없는 타입을 의미하며, 개발자가 특정 타입을 unknown에 알려주기 전까지는 string, number, symbol 등 어떤것이든 될 수 있는 가능성을 내포한다. 그러나 객체의 키값에는 symbol이 할당될 수 없다. 따라서 위와 같이 unknown 타입에 string으로 유니온 교집합을 해줘야 에러가 나지 않게 된다.
type Path<T> = T extends Record<string, unknown> ? PropertyPath<T> : never
interface TypeComponentProps<T extends Record<string, unknown>> {
obj: T
properties: Path<T>
}
이후 Path<T>를 만들어 PropertyPath<T>를 한번 감싸준다. 사실 이 과정을 생략해도 프로그램 동작에는 아무런 영향이 없다. 하지만 감싸주지 않을 시에는 아래와 같은 타입 설명이 매우 복잡해지는 현상이 발생한다.
아무래도 재귀를 돌면서 조합할 수 있는 모든 키 값을 찾아냈기 때문에 저렇게 더러워지지 않았나 싶다.
Path 타입으로 감싸주면 조잡한 타입 설명이 깔끔해진다.
컴포넌트 적용
만든 타입을 컴포넌트에 적용하는 건 쉽다. 아래와 같이 타입을 선언해준 후 컴포넌트에 적용해보자.
interface TypeComponentProps<T extends Record<string, unknown>> {
obj: T
properties: Path<T>
}
export const TypeInferComponent = <T extends Record<string, any>>({
obj,
properties,
}: TypeComponentProps<T>): ReactNode => {
const split = properties.split('.')
let value = { ...obj }
for (const property of split) {
value = value[property]
}
if (typeof value === 'string' || typeof value === 'number') {
return <div className="text-5xl font-semibold text-teal-500">{value}</div>
}
return <div>{String(value)}</div>
}
함수의 파라미터에 제네릭을 적용하는 방법은 크게 2가지가 있다.
- 함수 호출 시 제네릭 타입을 명시해주는 방식. (ex: function<string>("string") )
- 함수 호출 시 파라미터의 타입을 통해 제네릭을 자동으로 추론하는 방식. (ex: function("string") )
function identity<T>(arg: T):T {
return arg
}
let manually = identity<string>("문자열") // (1)직접 명시
let auto = identity(100) // (2) 알아서 추론
manually = 10101 // (error) 제네릭에 의해 manual의 타입은 string으로 정해짐
auto = "문자열" // (error) 제네릭에 의해 auto의 타입은 number로 정해짐
리액트 함수형 컴포넌트는 순도 100% 함수라고 볼 수 있지만, JSX 특유의 호출 방법 (ex: <Component /> )으로 인해 컴포넌트 선언 시 제네릭을 직접 명시해주는 방법을 취할 수 없다. 따라서 함수 호출 시 파라미터의 타입을 통해 제네릭을 자동으로 추론하게 하는 수 밖에 없다.
'Typescript' 카테고리의 다른 글
객체 키 타입의 유니온 추출 및 활용 (1) | 2024.01.09 |
---|---|
유틸리티 타입 (Parameters) (0) | 2023.11.21 |
유틸리티 타입 (Partial, Required, Record) (0) | 2023.10.27 |
keyof typeof typesomething (0) | 2023.09.05 |
React 컴포넌트에 타입스크립트 적용 (0) | 2023.09.05 |