How to make a generic type argument required in typescript?
Asked Answered
I

5

24

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?

Ingressive answered 1/11, 2018 at 21:50 Comment(0)
T
13

There is a simpler way of achieving the above, where:

  1. An explicit type parameter must be provided to pass through an argument without error, and
  2. A second explicit type parameter must be provided to get back a value that isn't 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.

Touraine answered 28/8, 2019 at 0:46 Comment(3)
In short, function foo<T=never>(bar: T) doesn't work because, when T is not specified, TypeScript will infer T from the type of bar. While function foo<T=never, U extends T=T>(bar: U) works because T cannot be automatically inferred and will default to never, and fail as we intended.Mcgann
I would not approve a PR that used this solution. It adds a great deal of pointless confusion and complexity. Quote, ERROR: Argument of type '"foo"' is not assignable to parameter of type 'never'. Someone who sees this error message will have absolutely no idea that the error is caused by not using a required generic. The error message not only does not inform the user of the problem, but misleads them. This is outrageously bad dx. Please don't put this in your code.Rollmop
I concede that this is real uglification but until we get prettier syntax I think the T,U=U approach is valid. I've seen this technique in the wild and it has a purpose. About the DX, yeah, I agree there, but while looking up this topic, I was able to find this thread, so someone who is confounded by related errors needs to improve their Google-Fu. I found another application for this where I don't think there is another solution - using an enum as a generic constraint on a function. Ref github.com/microsoft/TypeScript/issues/30611Noma
C
10

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()
Catabolite answered 2/11, 2018 at 0:57 Comment(1)
Ah, interesting, you expose a message through the type name, and it happens where expected!Ingressive
U
2

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

enter image description here

Uniparous answered 2/8, 2023 at 16:38 Comment(0)
M
1

This is how I make my first generic mandatory

const fn = <T = never, AT extends T = T>(arg: AT): any => {}
Metagnathous answered 26/1, 2023 at 21:4 Comment(1)
This is EXACTLY the same as the answer by @oleg in 2019.Noma
G
0

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
>(
Giantism answered 10/5 at 10:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.