Hur gör man ett generiskt typargument obligatoriskt i TypeScript?


Hur kan jag göra ett generiskt malltypargument obligatoriskt?

Hittills är det enda sättet jag har hittat för att göra det att använda never, men det orsakar ett fel på en annan plats än där generiken anropas.

TypeScript Playground-exemplet som klistrades in här:

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()

Som du ser i anropet till test1(), uppstår felet vid result.split('') eftersom never inte har en .split()-metod.

I test2 fungerar det jättebra när jag anger det generiska argumentet.

Hur kan jag göra argumentet obligatoriskt, och inte använda never, och få felet att inträffa vid anropet till sendWorkRequest om ett generiskt argument inte anges?

Svar #1

Det finns ett enklare sätt att uppnå ovanstående, där:

  1. En explicit typparameter måste anges för att kunna föra vidare ett argument utan fel, och
  2. En andra explicit typparameter måste anges för att få tillbaka ett värde som inte är 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();
}

Se denna TypeScript-lekplats.

Detta fungerar genom att låta TypeScript bara härleda den sista typparametern, samtidigt som never sätts som standard för de icke-härledda primära typparametrarna. Om explicita typparametrar inte skickas med uppstår ett fel eftersom värdet som skickas inte kan tilldelas standardvärdet never. Vad gäller returtypen är det ett utmärkt användande av unknown, eftersom den inte kommer att härledas till något annat om den inte uttryckligen parametriseras.

Svar #2

Se detta öppna förslag. Det bästa tillvägagångssätt jag känner till är att låta T standardiseras till never som du gjorde (förutsatt att never inte är ett giltigt typargument för T) och definiera typen för en av parametrarna i funktionen så att (1) om T anges som icke-never, då har parametern den typ du faktiskt vill ha, och (2) om T tillåts standardisera till never, då har parametern någon dummytyp som kommer att generera ett fel eftersom den inte matchar argumenttypen.

Den knepiga delen är att om en anropare sätter T till någon inom-scope typvariabel U av sin egen, vill vi tillåta anropet även om TypeScript inte kan utesluta att U kan vara never. För att hantera det fallet använder vi en hjälp-typ IfDefinitelyNever som utnyttjar förenklingsbeteendet hos indexerade accesstyper för att skilja ett definitivt never från en typvariabel. Den speciella G ("gate") parametern behövs för att förhindra att anropet från IfDefinitelyNever utvärderas för tidigt till sin falska gren i signaturen hos själva funktionen.

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()

Relaterade artiklar