Reputation: 45504
How can I make a generic template type argument required?
So far, the only way I found to do it is using never
but it causes an error to happen at a different place other than the callsite of the generic.
The TypeScript Playground example pasted here:
type RequestType =
| 'foo'
| 'bar'
| 'baz'
interface SomeRequest {
id: string
type: RequestType
sessionId: string
bucket: string
params: Array<any>
}
type ResponseResult = string | number | boolean
async function sendWorkRequest<T extends ResponseResult = never>(
type: RequestType,
...params
): Promise<T> {
await this.readyDeferred.promise
const request: SomeRequest = {
id: 'abc',
bucket: 'bucket',
type,
sessionId: 'some session id',
params: [1,'two',3],
}
const p = new Promise<T>(() => {})
this.requests[request.id] = p
this.worker.postMessage(request)
return p
}
// DOESN'T WORK
async function test1() {
const result = await sendWorkRequest('foo')
result.split('')
}
test1()
// WORKS
async function test2() {
const result = await sendWorkRequest<string>('foo')
result.split('')
}
test2()
As you see in the call to test1()
, the error happens at result.split('')
because never
does not have a .split()
method.
In test2
it works great when I provide the generic arg.
How can I make the arg required, and not use never, and for the error to happen on the call to sendWorkRequest
if a generic arg is not given?
Upvotes: 24
Views: 14264
Reputation: 866
Using
const fn = <T = never, AT extends T = T>(arg: AT): any => {}
as suggested by other answers causes a few problems. Mostly because the never type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never (except never itself).
So if your function returns this never
, Typescript will not raise an error if you assign it to some other type.
I prefer to use a type derived from a unique symbol, as this should safeguard against this assignment error:
declare const RequiredGenericArgument: unique symbol;
export const extractJson = <
A = typeof RequiredGenericArgument,
T extends z.Schema = z.Schema
>(
_str: string,
schema: T
): (A | undefined)[] => {
You can also use extends
by including the symbol in a union type:
export const extractJson = <
A extends
| YourType
| typeof RequiredGenericArgument = typeof RequiredGenericArgument,
T extends z.Schema = z.Schema
>(
Upvotes: 0
Reputation: 107
This isn't perfect but it provides an improved developer experience.
function myFunction<T = 'A type parameter is required.', AT extends T = T>(arg: AT) : any
Upvotes: 2
Reputation: 137
This is how I make my first generic mandatory
const fn = <T = never, AT extends T = T>(arg: AT): any => {}
Upvotes: 1
Reputation: 12682
There is a simpler way of achieving the above, where:
unknown
async function sendWorkRequest<ReqT = never, ResT = unknown, InferredReqT extends ReqT = ReqT>(
request: InferredReqT,
): Promise<ResT> {
return {} as ResT;
}
// Call does not succeed without an explicit request parameter.
async function test1() {
const result = await sendWorkRequest('foo');
// ~~~~~
// ERROR: Argument of type '"foo"' is not assignable to parameter of type 'never'
}
// Call succeeds, but response is 'unknown'.
async function test2() {
const result: number = await sendWorkRequest<string>('foo');
// ~~~~~~
// ERROR: Type 'unknown' is not assignable to type 'number'.
result.valueOf();
}
// Call succeeds and returns expected response.
async function test3() {
const result = await sendWorkRequest<string, number>('foo');
result.valueOf();
}
See this TypeScript playground.
This works by having TypeScript infer only the last type parameter, while setting never
as the default for the non-inferred primary type parameters. If explicit type parameters are not passed in, an error occurs because the value passed in is not assignable to the default never
. As for the return type, it's a great use of unknown
, as it won't be inferred to anything else unless explicitly parameterized.
Upvotes: 13
Reputation: 30959
See this open suggestion. The best approach I know of is to let T
default to never
as you did (assuming that never
is not a valid type argument for T
) and define the type of one of the parameters to the function so that (1) if T
is specified as non-never
, then the parameter has the type you actually want, and (2) if T
is allowed to default to never
, then the parameter has some dummy type that will generate an error because it doesn't match the argument type.
The tricky part is that if a caller sets T
to some in-scope type variable U
of its own, we want to allow the call even though TypeScript cannot rule out that U
could be never
. To handle that case, we use a helper type IfDefinitelyNever
that abuses the simplification behavior of indexed access types to distinguish a definite never
from a type variable. The special G
("gate") parameter is needed to prevent the call from IfDefinitelyNever
from prematurely evaluating to its false branch in the signature of the function itself.
type RequestType =
| 'foo'
| 'bar'
| 'baz'
interface SomeRequest {
id: string
type: RequestType
sessionId: string
bucket: string
params: Array<any>
}
type ResponseResult = string | number | boolean
const ERROR_INTERFACE_DUMMY = Symbol();
interface Type_parameter_T_is_required {
[ERROR_INTERFACE_DUMMY]: never;
}
interface Do_not_mess_with_this_type_parameter {
[ERROR_INTERFACE_DUMMY]: never;
}
type IfDefinitelyNever<X, A, B, G extends Do_not_mess_with_this_type_parameter> =
("good" | G) extends {[P in keyof X]: "good"}[keyof X] ? B : ([X] extends [never] ? A : B);
async function sendWorkRequest<T extends ResponseResult = never,
G extends Do_not_mess_with_this_type_parameter = never>(
type: RequestType & IfDefinitelyNever<T, Type_parameter_T_is_required, unknown, G>,
...params
): Promise<T> {
await this.readyDeferred.promise
const request: SomeRequest = {
id: 'abc',
bucket: 'bucket',
type,
sessionId: 'some session id',
params: [1,'two',3],
}
const p = new Promise<T>(() => {})
this.requests[request.id] = p
this.worker.postMessage(request)
return p
}
// DOESN'T WORK
async function test1() {
// Error: Argument of type '"foo"' is not assignable to parameter of type
// '("foo" & Type_parameter_T_is_required) |
// ("bar" & Type_parameter_T_is_required) |
// ("baz" & Type_parameter_T_is_required)'.
const result = await sendWorkRequest('foo')
result.split('')
}
test1()
// WORKS
async function test2() {
const result = await sendWorkRequest<string>('foo')
result.split('')
}
test2()
// ALSO WORKS
async function test3<U extends ResponseResult>() {
const result = await sendWorkRequest<U>('foo')
}
test3()
Upvotes: 10