I want to define whether a function should contain an argument via an interface. The library I'm developing calls for many different methods to be generated, and hardcoding those methods would require too much maintenance; so I figured that types would be a good place to define such things.
Perhaps this is best explained with code. Here's a library that abstracts some rest API:
interface RequestInterface {
endpoint: string
body?: unknown
}
interface GetPosts extends RequestInterface {
endpoint: '/posts'
body: never
}
interface CreatePost extends RequestInterface {
endpoint: '/posts'
body: string
}
function Factory<R extends RequestInterface> (endpoint: R['endpoint']) {
return (body?: R['body']): void => {
console.log(`Hitting ${endpoint} with ${body}`)
}
}
const myLibrary = {
getPosts: Factory<GetPosts>('/posts'),
createPosts: Factory<CreatePost>('/posts'),
}
myLibrary.getPosts('something') // => Correctly errors
myLibrary.createPosts(999) // => Correctly errors
myLibrary.createPosts() // => I want this to error
In the above, I'm defining the endpoint
and body
of a particular type of request in my interfaces. Although the TypeScript compiler correctly guards me against passing the wrong argument types, it doesn't guard me against not passing a value when one is required.
I understand why TypeScript doesn't error (because the method defined in factory can be undefined according to my typings), but I figured the above code was a good way of describing what I want to achieve: a quick, declarative library of methods which satisfy a particular type.
A Possible Solution
If I'm willing to extend my interfaces from two separate interfaces (one or the other) then I can achieve something close to what I want using Construct Signatures:
interface RequestInterface {
endpoint: string
call: () => void
}
interface RequestInterfaceWithBody {
endpoint: string
call: {
(body: any): void
}
}
interface GetPosts extends RequestInterface {
endpoint: '/posts'
}
interface CreatePost extends RequestInterfaceWithBody {
endpoint: '/posts'
call: {
(body: string): void
}
}
function Factory<R extends RequestInterface|RequestInterfaceWithBody> (endpoint: R['endpoint']): R['call'] {
return (body): void => {
console.log(`Hitting ${endpoint} with ${body}`)
}
}
const myLibrary = {
getPosts: Factory<GetPosts>('/posts'),
createPosts: Factory<CreatePost>('/posts'),
}
myLibrary.getPosts() // => Correctly passes
myLibrary.getPosts('something') // => Correctly errors
myLibrary.createPosts(999) // => Correctly errors
myLibrary.createPosts() // => Correctly errors
myLibrary.createPosts('hi') // => Correctly passes
Aside from the fact that I need to pick between two "super" types before extending anything, a major problem with this is that the Construct Signature argument is not very accessible.
Although not demonstrated in the example, the types I create are also used elsewhere in my codebase, and the body
is accessible (i.e GetPosts['body']
). With the above, it is not easy to access and I'll probably need to create a separate re-usable type definition to achieve the same thing.
CodePudding user response:
You almost hit the spot with your initial types. Two changes required:
- Make
body
ofGetPosts
of typevoid
- Make
body
of returned function required
interface RequestInterface {
endpoint: string;
body?: unknown;
}
interface GetPosts extends RequestInterface {
endpoint: "/posts";
body: void;
}
interface CreatePost extends RequestInterface {
endpoint: "/posts";
body: string;
}
function Factory<R extends RequestInterface>(endpoint: R["endpoint"]) {
return (body: R["body"]): void => {
console.log(`Hitting ${endpoint} with ${body}`);
};
}
const myLibrary = {
getPosts: Factory<GetPosts>("/posts"),
createPosts: Factory<CreatePost>("/posts"),
};
// @ts-expect-error
myLibrary.getPosts("something");
// @ts-expect-error
myLibrary.createPosts(999);
// @ts-expect-error
myLibrary.createPosts();
myLibrary.getPosts();
myLibrary.createPosts("Hello, StackOverflow!");
Explanation
never
type tells compiler that this should never happen. So, it someone tries to use GetPosts
, it's an error, since it should never happen. void
(undefined
in this case should be also fine) tells that value should not be there.
Making body
required in returned function makes it required. But since it is void
for GetPosts
, you can call it like myLibrary.getPosts(undefined)
or simply myLibrary.getPosts()
which is equivalent