props에 허용하는 클래스명 강제하기
서사
컴포넌트 Props에 tailwindcss의 클래스명을 받을 때가 있다. 타입 선언할 때 단순하게 string으로만 정의하기도 하는데, string으로 정의해서 의도하지 않게 버그가 발생하는 경우가 있었다. 예를 들어서 아이콘 컴포넌트에서 w-*, h-*를 함께 받아야 하는데, string으로만 정의하면 컴파일 타임에 알 수 없다.
타입스크립트의 탬플릿 리터럴과 infer를 활용하면 사용하면 안되는 클래스명과 쌍으로 사용해야 하는 클래스명을 타입을 통해 알 수 있다.
Tailwind ClassName fill-*
대신 text-
사용하도록 처리
fill-*를 사용하면 text-*로 사용하라고 알려준다.
ts
T extends `${infer U}fill${infer V}` ? `${U}text${V}` : T
Pair ClassName
항상 쌍으로 사용해야하는 클래스명의 타입을 정의할 때 사용하는 타입니다.
타입 정의
ts
type SizeClass<
U extends string,
V extends string,
Size extends string,
Left extends string,
Right extends string,
> = `${U}${Left}${Size} ${Right}${Size}${V}`;
type CheckPair<
T,
U extends string,
V extends string,
Size extends string,
Left extends string,
Right extends string,
> = T extends `${infer U}${Left}${Size} ${Right}${Size}${infer V}`
? SizeClass<U, V, Size, Left, Right>
: SizeClass<U, ` ${V}`, Size, Left, Right>;
export type RequirePair<
T,
Left extends string,
Right extends string,
> = T extends `${infer U}${Left}${infer Size} ${infer V}`
? CheckPair<T, U, V, Size, Left, Right>
: T extends `${infer U}${Right}${infer Size} ${infer V}`
? CheckPair<T, U, V, Size, Left, Right>
: T extends `${infer U}${Left}${infer Size}`
? SizeClass<U, "", Size, Left, Right>
: T extends `${infer U}${Right}${infer Size}`
? SizeClass<U, "", Size, Left, Right>
: T;
사용 예제
ts
type Props<T> = {
className: RequirePair<T, 'w-', 'h-'>
}
function something <T extends string> ({ className }: Props<T>) {}
// w-, h- 둘 중 하나를 사용하지 않아 에러 미발생
something({className: 'text-lg'});
// w- 하나만 사용해서 에러 발생
// Type '"text-lg w-10"' is not assignable to type '"text-lg w-10 h-10"'.
something({className: 'text-lg w-10'});
Tailwind ClassName Prefix Filter
타입 정의
ts
type FilterString<
S extends string,
Target extends string,
> = S extends `${Target}${string | ""}` ? "" : S;
type Split<
S extends string,
D extends string,
Prefix extends string,
> = S extends ""
? []
: S extends `${infer T}${D}${infer U}`
? [`${FilterString<T, Prefix>}`, ...Split<U, D, Prefix>]
: [`${FilterString<S, Prefix>}`];
type Join<S extends string[], D extends string> = S extends [
infer Head,
...infer Tail,
]
? `${Head extends string ? Head : ""}${Head extends "" ? "" : D}${Join<
Tail extends string[] ? Tail : [],
D
>}`
: "";
type TrimRight<T extends string> = T extends `${infer R} `
? `${TrimRight<R>}`
: T;
사용 예제
ts
export type DisallowClassName<
S extends string,
Prefix extends string,
> = TrimRight<Join<Split<S, " ", Prefix>, " ">>;
// h-10 w-10 m-10 p-10
type FilteredClassName0 = DisallowClassName<
"h-10 w-10 m-10 p-10 fill-black",
"fill"
>;
type FilteredClassName1 = DisallowClassName<
"h-10 w-10 fill-black m-10 p-10",
"fill"
>;
type FilteredClassName2 = DisallowClassName<
"fill-black h-10 w-10 m-10 p-10",
"fill"
>;
// fill-black m-10 p-10
type FilteredClassName3 = DisallowClassName<
"fill-black h-10 w-10 m-10 p-10",
"h" | "w"
>;