Tipos condicionales de TypeScript: filtrar propiedades de solo lectura/seleccionar solo propiedades requeridas

Resuelto DanielM asked hace 6 años • 2 respuestas

Usando los nuevos tipos condicionales en TypeScript (o tal vez otra técnica), ¿hay alguna manera de elegir solo ciertas propiedades de una interfaz en función de sus modificadores? Por ejemplo, tener...

interface I1 {
    readonly n: number
    s: string
}

Me gustaría crear un nuevo tipo basado en el anterior que se ve así:

interface I2 {
    s: string
}
DanielM avatar Mar 31 '18 00:03 DanielM
Aceptado

Actualización 2018-10: @MattMcCutchen ha descubierto que es posible detectar readonlycampos (invalidando el pasaje tachado a continuación), como se muestra en esta respuesta . Aquí hay una manera de construirlo:

type IfEquals<X, Y, A=X, B=never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? A : B;

type WritableKeys<T> = {
  [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
}[keyof T];

type ReadonlyKeys<T> = {
  [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>
}[keyof T];

Si desea extraer los campos de escritura de una interfaz, puede utilizar la WritableKeysdefinición anterior y Pickjuntos:

interface I1 {
    readonly n: number
    s: string
}

type I2 = Pick<I1, WritableKeys<I1>>; 
// equivalent to { s: string; }

¡Hurra!

Para "solo lectura", no creo que puedas extraerlos. [Miré este problema antes] (https://github.com/Microsoft/TypeScript/issues/13257#issuecomment-308528175) y no fue posible en ese momento; y no creo que haya cambiado nada.

Dado que el compilador no verifica minuciosamente readonlylas propiedades , siempre puedes asignar {readonly n: number}a a {n: number}y viceversa. Y, por lo tanto, la verificación obvia del tipo condicional TSv2.8 no funciona. Si, por ejemplo, {n: number}no se consideraran asignables {readonly n: number}, podría hacer algo como:

// does not work, do not try this
type ExcludeReadonlyProps<T> = Pick<T,
  { [K in keyof T]-?:
    ({ readonly [P in K]: T[K] } extends { [P in K]: T[K] } ? never : K)
  }[keyof T]>

type I2 = ExcludeReadonlyProps<I1> // should be {s: string} but is {} 🙁

Pero no puedes. Hay una discusión interesante sobre esto en una edición de GitHub originalmente llamada " readonlylos modificadores son una broma" .

¡Lo siento! Buena suerte.


Para las propiedades opcionales, puede detectarlas y, por lo tanto, extraerlas o excluirlas. La idea aquí es que se {}extiende {a?: string}, pero {}no se extiende {a: string}ni siquiera {a: string | undefined}. A continuación se explica cómo se puede crear una manera de eliminar propiedades opcionales de un tipo:

type RequiredKeys<T> = { [K in keyof T]-?:
  ({} extends { [P in K]: T[K] } ? never : K)
}[keyof T]

type OptionalKeys<T> = { [K in keyof T]-?:
  ({} extends { [P in K]: T[K] } ? K : never)
}[keyof T]

type ExcludeOptionalProps<T> = Pick<T, RequiredKeys<T>>

type I3 = { 
  a: string, 
  b?: number, 
  c: boolean | undefined
}

type I4 = ExcludeOptionalProps<I3>;
// {a: string; c: boolean | undefined} 🙂

Entonces eso es bueno.


Finalmente, no sé si desea poder hacer cosas con modificadores de propiedad de clase exclusiva como public, private, protectedy abstract, pero lo dudaría. Sucede que las propiedades de la clase privatey protectedse pueden excluir con bastante facilidad, ya que no están presentes en keyof:

class Foo {
  public a = ""
  protected b = 2
  private c = false
}
type PublicOnly<T> = Pick<T, keyof T>; // seems like a no-op but it works
type PublicFoo = PublicOnly<Foo>; // {a: string} 🙂

Pero extraer las propiedades privateo protectedpuede ser imposible, por la misma razón por la que excluirlas es tan fácil: keyof Foono las tiene. Y para todos estos, incluido abstract, no puede agregarlos a propiedades en alias de tipo (son modificadores solo de clase), por lo que no se me ocurre mucho que hacer para tocarlos.

jcalz avatar Mar 30 '2018 18:03 jcalz

He refactorizado los tipos en la solución aceptada y algunos pueden preferir mis tipos porque son más simples:

/**
 * Returns the required keys of an object
 * @example
 * type U = RequiredKeys<{ a?: 'a'; b: 'b'; c: 'a' }> // "b" | "c"
 */
export type RequiredKeys<T extends object> = keyof {
  [K in keyof T as T extends Record<K, T[K]> ? K : never]: K
}
/**
 * Returns the optional keys of an object
 * @example
 * type U = OptionalPropertyOf<{ a?: 'a'; b: 'b', c?: 'a' }> // "a" | "c"
 */
export type OptionalKeys<T extends object> = keyof {
  [K in keyof T as T extends Record<K, T[K]> ? never : K]: K
}
TrevTheDev avatar May 07 '2023 03:05 TrevTheDev