재귀로 객체 내 모든 키값을 추출하는 타입 만들기
객체 내 모든 키를 추출할 수 있는 타입을 만든 후, 컴포넌트에 적용하여 컴포넌트가 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/
JavaScript With Syntax For Types.
TypeScript extends JavaScript by adding types to the language. TypeScript speeds up your development experience by catching errors and providing fixes before you even run your code.
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 |