Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 485
trusktr Avatar asked Nov 01 '18 21:11

trusktr


People also ask

How do you pass a generic type as parameter TypeScript?

Assigning Generic ParametersBy passing in the type with the <number> code, you are explicitly letting TypeScript know that you want the generic type parameter T of the identity function to be of type number . This will enforce the number type as the argument and the return value.

How do you define a generic type in TypeScript?

Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.

How do I use generic classes in TypeScript?

TypeScript supports generic classes. The generic type parameter is specified in angle brackets after the name of the class. A generic class can have generic fields (member variables) or methods. In the above example, we created a generic class named KeyValuePair with a type variable in the angle brackets <T, U> .

How do I create a generic object in TypeScript?

To specify generic object type in TypeScript, we can use the Record type. const myObj: Record<string, any> = { //... }; to set myObj to the Record type with string keys and any type for the property values.


2 Answers

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()
like image 61
Matt McCutchen Avatar answered Dec 01 '22 01:12

Matt McCutchen


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.

like image 40
Oleg Vaskevich Avatar answered Dec 01 '22 01:12

Oleg Vaskevich