Quiero definir si una función debe contener un argumento a través de una interfaz. La biblioteca que estoy desarrollando requiere que se generen muchos métodos diferentes, y codificar esos métodos requeriría demasiado mantenimiento; así que pensé que los tipos serían un buen lugar para definir tales cosas.

Quizás esto se explique mejor con código. Aquí hay una biblioteca que abstrae algunas API de descanso:

interface RequestInterface {
  endpoint: string
  body?: unknown
}

interface GetPosts extends RequestInterface {
  endpoint: '/posts'
  body: never
}

interface CreatePost extends RequestInterface {
  endpoint: '/posts'
  body: string
}

function Factory<R extends RequestInterface> (endpoint: R['endpoint']) {

  return (body?: R['body']): void => {

    console.log(`Hitting ${endpoint} with ${body}`)

  }
}

const myLibrary = {

  getPosts: Factory<GetPosts>('/posts'),
  createPosts: Factory<CreatePost>('/posts'),

}

myLibrary.getPosts('something') // => Correctly errors
myLibrary.createPosts(999)      // => Correctly errors
myLibrary.createPosts()         // => I want this to error

En lo anterior, estoy definiendo el endpoint y body de un tipo particular de solicitud en mis interfaces. Aunque el compilador de TypeScript me protege correctamente contra pasar los tipos de argumentos incorrectos , no me protege contra no pasar un valor cuando se requiere uno.

Entiendo por qué TypeScript no produce errores (porque el método definido en la fábrica puede no estar definido de acuerdo con mis tipificaciones), pero pensé que el código anterior era una buena forma de describir lo que quiero lograr: una biblioteca rápida y declarativa de métodos que satisfacen un tipo en particular.

Una posible solución

Si estoy dispuesto a extender mis interfaces desde dos interfaces separadas (una u otra), entonces puedo lograr algo cercano a lo que quiero usando Construir firmas:

interface RequestInterface {
  endpoint: string
  call: () => void
}

interface RequestInterfaceWithBody {
  endpoint: string
  call: {
    (body: any): void
  }
}

interface GetPosts extends RequestInterface {
  endpoint: '/posts'
}

interface CreatePost extends RequestInterfaceWithBody {
  endpoint: '/posts'
  call: {
    (body: string): void
  }
}

function Factory<R extends RequestInterface|RequestInterfaceWithBody> (endpoint: R['endpoint']): R['call'] {

  return (body): void => {

    console.log(`Hitting ${endpoint} with ${body}`)

  }
}

const myLibrary = {

  getPosts: Factory<GetPosts>('/posts'),
  createPosts: Factory<CreatePost>('/posts'),

}

myLibrary.getPosts()            // => Correctly passes
myLibrary.getPosts('something') // => Correctly errors
myLibrary.createPosts(999)      // => Correctly errors
myLibrary.createPosts()         // => Correctly errors
myLibrary.createPosts('hi')     // => Correctly passes

Aparte del hecho de que necesito elegir entre dos tipos "super" antes de extender algo, un problema importante con esto es que el argumento Construct Signature no es muy accesible.

Aunque no se demuestra en el ejemplo, los tipos que creo también se usan en otros lugares de mi base de código, y el body es accesible (es decir, GetPosts['body']). Con lo anterior, no es fácil acceder y probablemente necesitaré crear una definición de tipo reutilizable separada para lograr lo mismo.

0
shennan 27 nov. 2021 a las 12:06
¿Alguna razón en particular para el voto negativo?
 – 
shennan
27 nov. 2021 a las 12:19
¿Tiene habilitado el modo estricto de TypeScript? Puede hacerlo estableciendo la opción "estricta" en verdadero en tsconfig.ts.
 – 
Jesse Schokker
27 nov. 2021 a las 12:19

1 respuesta

La mejor respuesta

Casi da en el clavo con sus tipos iniciales. Se requieren dos cambios:

  1. Haga body de GetPosts de tipo void
  2. Hacer body de la función devuelta requerida
interface RequestInterface {
  endpoint: string;
  body?: unknown;
}

interface GetPosts extends RequestInterface {
  endpoint: "/posts";
  body: void;
}

interface CreatePost extends RequestInterface {
  endpoint: "/posts";
  body: string;
}

function Factory<R extends RequestInterface>(endpoint: R["endpoint"]) {
  return (body: R["body"]): void => {
    console.log(`Hitting ${endpoint} with ${body}`);
  };
}

const myLibrary = {
  getPosts: Factory<GetPosts>("/posts"),
  createPosts: Factory<CreatePost>("/posts"),
};

// @ts-expect-error
myLibrary.getPosts("something");
// @ts-expect-error
myLibrary.createPosts(999);
// @ts-expect-error
myLibrary.createPosts();

myLibrary.getPosts();
myLibrary.createPosts("Hello, StackOverflow!");

TS Playground

Explicación

El tipo never le dice al compilador que esto nunca debería suceder. Entonces, si alguien intenta usar GetPosts, es un error, ya que nunca debería suceder. void (undefined en este caso también debería estar bien) indica que el valor no debería estar allí.

Hacer que body sea obligatorio en la función devuelta lo hace obligatorio. Pero como es void para GetPosts, puede llamarlo como myLibrary.getPosts(undefined) o simplemente myLibrary.getPosts() que es equivalente

0
Mr. Hedgehog 27 nov. 2021 a las 13:17
¡Gracias! Sigo sintiéndome honrado por el conocimiento de la gente de TypeScript y la cobertura del lenguaje en sí.
 – 
shennan
27 nov. 2021 a las 13:18
Para ser honesto, no conozco muy bien TypeScript. Copié tu código en el patio de recreo y lo manipulé hasta que funcionó. Solo entonces comprendí por qué funcionaba y qué estaba mal. Todo lo que queda: copie su código una vez más y aplíquele las partes relevantes, para que no sea muy diferente.
 – 
Mr. Hedgehog
27 nov. 2021 a las 13:23
¡Entonces déjame al menos felicitarte por tu honestidad y tenacidad!
 – 
shennan
27 nov. 2021 a las 13:30