타입 안전한 라우터 — 경로 매개변수를 타입으로 추론
타입 안전한 라우터는
/users/:id/posts/:postId같은 경로 패턴에서:id와:postId를 자동으로 추출 해서 타입으로 만듭니다.
문제: 일반적인 라우터
// 타입 없는 라우터
router.get('/users/:id', (req) => {
const id = req.params.id; // string — 어떤 params가 있는지 모름
const typo = req.params.idd; // ⚠️ 오타인데 에러 없음
});
경로에서 매개변수 추출
Template Literal Types와 infer로 경로 문자열에서 매개변수를 추출합니다.
// ':param'을 찾아서 추출
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;
type Params1 = ExtractParams<'/users/:id'>;
// 'id'
type Params2 = ExtractParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'
type Params3 = ExtractParams<'/about'>;
// never (매개변수 없음)
// 매개변수를 Record로 변환
type RouteParams<Path extends string> =
ExtractParams<Path> extends never
? {}
: Record<ExtractParams<Path>, string>;
type P1 = RouteParams<'/users/:id'>;
// { id: string }
type P2 = RouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }
타입 안전한 라우터 구현
type Handler<Path extends string> = (
req: {
params: RouteParams<Path>;
query: Record<string, string>;
},
res: {
json: (data: unknown) => void;
status: (code: number) => { json: (data: unknown) => void };
}
) => void;
class TypedRouter {
private routes: Map<string, Function> = new Map();
get<Path extends string>(path: Path, handler: Handler<Path>): this {
this.routes.set(`GET ${path}`, handler);
return this;
}
post<Path extends string>(path: Path, handler: Handler<Path>): this {
this.routes.set(`POST ${path}`, handler);
return this;
}
}
const router = new TypedRouter();
// ✅ params가 자동 추론됨
router.get('/users/:id', (req, res) => {
const { id } = req.params; // id: string — 자동 완성됨
res.json({ userId: id });
});
router.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params; // 둘 다 자동 완성
res.json({ userId, postId });
});
// ❌ 존재하지 않는 매개변수 접근 시 에러
router.get('/users/:id', (req, res) => {
// const name = req.params.name; // ❌ Error — name은 없음
});
타입 안전한 링크 생성
// 라우트 정의
type Routes = {
'/': {};
'/users': {};
'/users/:id': { id: string };
'/users/:id/posts/:postId': { id: string; postId: string };
};
// 매개변수를 치환해서 실제 URL 생성
type BuildPath<
Path extends string,
Params extends Record<string, string>
> = Path extends `${infer Before}:${infer Param}/${infer After}`
? Param extends keyof Params
? `${Before}${Params[Param]}/${BuildPath<After, Params>}`
: never
: Path extends `${infer Before}:${infer Param}`
? Param extends keyof Params
? `${Before}${Params[Param]}`
: never
: Path;
// 타입 안전한 링크 함수
function buildLink<Path extends keyof Routes>(
path: Path,
...args: Routes[Path] extends Record<string, never>
? []
: [params: Routes[Path]]
): string {
let result: string = path;
if (args.length > 0) {
const params = args[0] as Record<string, string>;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value);
}
}
return result;
}
// 사용
buildLink('/'); // '/'
buildLink('/users/:id', { id: '42' }); // '/users/42'
buildLink('/users/:id/posts/:postId', { id: '1', postId: '99' }); // '/users/1/posts/99'
// buildLink('/users/:id'); // ❌ Error — params 누락
// buildLink('/users/:id', { name: 'test' }); // ❌ Error — 잘못된 params
Next.js에서의 활용
// Next.js App Router의 경로를 타입으로 관리
type AppRoutes = {
'/': {};
'/about': {};
'/blog/[slug]': { slug: string };
'/users/[id]/settings': { id: string };
};
// 타입 안전한 Link 컴포넌트 래퍼를 만들 수 있음
정리
- Template Literal Types + infer로 라우트 경로에서 매개변수를 자동 추출한다
- 경로별 params 타입이 자동 추론되어 오타를 컴파일 타임에 잡는다
- 링크 생성 함수도 타입 안전하게 만들 수 있다
- tanstack/router, Hono 등이 이 패턴을 활용하는 대표적인 라이브러리다
댓글 로딩 중...