trusktr
trusktr

Reputation: 45504

How to make a generic type argument required in typescript?

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

Answers (5)

Brendon Boshell
Brendon Boshell

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

nathanwinder
nathanwinder

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

enter image description here

Upvotes: 2

Edgar Abgaryan
Edgar Abgaryan

Reputation: 137

This is how I make my first generic mandatory

const fn = <T = never, AT extends T = T>(arg: AT): any => {}

Upvotes: 1

Oleg Vaskevich
Oleg Vaskevich

Reputation: 12682

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.

Upvotes: 13

Matt McCutchen
Matt McCutchen

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

Related Questions