/**
* Add code branches covering the following invalid cases:
* - No method was provided.
* - An unsupported method was provided.
* - A valid method was provided, but a path is missing.
* Feel free to use the following types:
*/
type NoMethodError = "Method missing.";
type UnknownMethodError<M extends string> = `'${M}' isn't a supported method.`
type PathMissingError<T extends string> = `A path is missing after '${T}'.`;
type NoSpacesError = "Spaces aren't allowed in route paths.";
type InvalidPathError = "This path is invalid.";
type Method = "GET" | "POST" | "PUT" | "PATCH";
type NoPathError<M extends string> = `A path is missing after '${M}'.`
type InvalidMethodError<M extends string> = `'${M}' isn't a supported method.`
type ValidateRoute<Path> =
Path extends `${Method} ${string} ${string}` ? NoSpacesError
: Path extends `${infer M} ` ? NoPathError<M>
: Path extends `/${string}` ? "Method missing."
: Path extends `${Method} ${string}` ? Path
: Path extends `${infer M} ${string}` ? InvalidMethodError<M>
: InvalidPathError;
type res1 = ValidateRoute<"GET ">;
type test1 = Expect<Equal<res1, "A path is missing after 'GET'.">>;
type res2 = ValidateRoute<"POST ">;
type test2 = Expect<Equal<res2, "A path is missing after 'POST'.">>;
type res3 = ValidateRoute<"/no/method">;
type test3 = Expect<Equal<res3, "Method missing.">>;
type res4 = ValidateRoute<"OOPS /unknown-method">;
type test4 = Expect<Equal<res4, "'OOPS' isn't a supported method.">>;
type res5 = ValidateRoute<"GET /spaces spaces">;
type test5 = Expect<Equal<res5, NoSpacesError>>;
type res6 = ValidateRoute<"GET /valid-route">;
type test6 = Expect<Equal<res6, "GET /valid-route">>;
export {};
/**
* Make the `body` parameter type-safe!
* Given the following predicates:
*/
declare const isComment: (x: unknown) => x is Comment;
declare const isVideo: (x: unknown) => x is Video;
/* `body` should be correctly narrowed: */
route("POST /comments")
.validateBody(isComment)
.handle(({ body }) => {
type test = Expect<Equal<typeof body, Comment>>;
return "✅";
});
route("PUT /video/:id")
.validateBody(isVideo)
.handle(({ body }) => {
type test = Expect<Equal<typeof body, Video>>;
return "✅";
});
/**
* Modify the `route` function and its methods
* to keep track of the type of `body`! 👇
*/
type RouteStateConstraint = {
method: Method;
isValid: boolean;
body: unknown;
};
type InitialRouteState<P> = {
method: GetMethod<P>;
isValid: GetMethod<P> extends "GET" ? true : false;
body: GetMethod<P> extends "GET" ? never : unknown;
};
declare function route<const P extends string>(
path: P,
): Route<InitialRouteState<P>>;
/*
We have extracted the types of `handle` and `validateBody`
to separate generic types for readability: */
type Route<State extends RouteStateConstraint> = {
isAuthenticated: () => Route<State>;
isAdmin: () => Route<State>;
validateBody: ValidateBodyMethod<State>;
handle: HandleMethod<State>;
};
type ValidateBodyMethod<State extends RouteStateConstraint> =
State["method"] extends "GET"
? GETBodyError
: <Body>(
isValid: (body: unknown) => body is Body,
) => Route<Assign<State, { isValid: true; body: Body }>>;
type ParsedRequest<B> = {
cookies: Record<string, string>;
params: Record<string, string>;
body: B;
};
type HandleMethod<State extends RouteStateConstraint> =
State["isValid"] extends false
? UnvalidatedBodyError<State["method"]>
: (fn: (req: ParsedRequest<State["body"]>) => string | Promise<string>) => Route<State>;
/**
* Helper types and functions
*/
type Comment = { content: string, author: string };
type Video = { src: string, title: string };
type Method = "GET" | "POST" | "PUT" | "PATCH";
type GetMethod<P> = P extends `${infer M extends Method} ${string}` ? M : never;
type Assign<A, B> = Compute<Omit<A, keyof B> & B>;
type GETBodyError = ".validateBody(...) isn't available on GET routes.";
type UnvalidatedBodyError<M extends Method> =
`You must call .validateBody(...) before .handle(...) on ${M} routes.`;
export {};
/**
* Make sure `isAuthenticated()` and `isAdmin()`
* are always used correctly!
*/
route("GET /tricks/:trickId/details")
.isAuthenticated()
// @ts-expect-error ❌ already authenticated.
.isAuthenticated()
route("GET /admin/dashboard")
// @ts-expect-error ❌ not authenticated.
.isAdmin()
route("GET /admin/dashboard")
.isAuthenticated()
.isAdmin()
// @ts-expect-error ❌ already admin.
.isAdmin()
route("GET /admin/dashboard")
.isAuthenticated()
.isAdmin()
.handle(() => `📈`); // ✅
/**
* Modify the `route` function and its methods 👇
*/
// Use these error messages
type SeveralAuthError = ".isAuthenticated() can only be called once."
type SeveralAdminError = ".isAdmin() can only be called once."
type UnauthenticatedAdminError = "You must call .isAuthenticated() before .isAdmin()."
type RouteStateConstraint = {
method: Method;
isValid: boolean;
body: unknown;
isAuthenticated: boolean;
isAdminCalled: boolean;
};
type InitialRouteState<Path, Body> = {
method: GetMethod<Path>;
isValid: GetMethod<Path> extends "GET" ? true : false;
body: Body;
isAuthenticated: false;
isAdminCalled: false;
};
declare function route<const Path extends string>(
path: Path,
): Route<InitialRouteState<Path, unknown>>;
type Route<State extends RouteStateConstraint> = {
isAuthenticated: IsAuthenticatedMethod<State>;
isAdmin: IsAdminMethod<State>;
validateBody: ValidateBodyMethod<State>;
handle: HandleMethod<State>;
};
type IsAuthenticatedMethod<State extends RouteStateConstraint> =
State["isAuthenticated"] extends true
? SeveralAuthError
: () => Route<Assign<State, {isAuthenticated: true}>>
type IsAdminMethod<State extends RouteStateConstraint> =
State["isAuthenticated"] extends false
? UnauthenticatedAdminError
: State["isAdminCalled"] extends true
? SeveralAdminError
: () => Route<Assign<State, {isAdminCalled: true}>>
/**
* Helper types and functions
*/
type ValidateBodyMethod<State extends RouteStateConstraint> =
State["method"] extends "GET"
? GETBodyError
: <Body>(
isValid: (body: unknown) => body is Body,
) => Route<Assign<State, { isValid: true; body: Body }>>;
type ParsedRequest<Body> = {
cookies: Record<string, string>;
params: Record<string, string>;
body: Body;
};
type HandleMethod<State extends RouteStateConstraint> =
State["isValid"] extends false
? UnvalidatedBodyError<State["method"]>
: (
fn: (req: ParsedRequest<State["body"]>) => string | Promise<string>,
) => Route<State>;
type Method = "GET" | "POST" | "PUT" | "PATCH";
type GetMethod<P> = P extends `${infer M extends Method} ${string}` ? M : never;
type Assign<A, B> = Compute<Omit<A, keyof B> & B>;
type GETBodyError = ".validateBody(...) isn't available on GET routes.";
type UnvalidatedBodyError<M extends Method> =
`You must call .validateBody(...) before .handle(...) on ${M} routes.`;
export {};
/**
* Implement `IsShadowing`:
*/
export type IsShadowing<Path1, Path2> = Path1 extends Path2
? true
: Path1 extends `${infer A}:${string}`
? Path2 extends `${A}:${string}`
? true
: false
: false;
/**
* Does shadow:
*/
type res1 = IsShadowing<"GET /", "GET /">;
type test1 = Expect<Equal<res1, true>>;
type res2 = IsShadowing<"GET /user/:name", "GET /user/:username">;
type test2 = Expect<Equal<res2, true>>;
type res3 = IsShadowing<"GET /user/:name", "GET /user/:name/profile">;
type test3 = Expect<Equal<res3, true>>;
/**
* Does not shadow:
*/
type res4 = IsShadowing<"GET /", "GET /users">;
type test4 = Expect<Equal<res4, false>>;
type res5 = IsShadowing<"GET /trick/:trickId", "GET /">;
type test5 = Expect<Equal<res5, false>>;
type res6 = IsShadowing<"GET /trick/:trickId", "PUT /trick/:trickId">;
type test6 = Expect<Equal<res6, false>>;
마지막 문제는 깔끔하게 포기. 이런게 가능하다는 것 까지만 알고.. 나중에 다시 보자.