Tipos condicionales de TypeScript: filtrar propiedades de solo lectura/seleccionar solo propiedades requeridas
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
}
Actualización 2018-10: @MattMcCutchen ha descubierto que es posible detectar readonly
campos (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 WritableKeys
definición anterior y Pick
juntos:
interface I1 {
readonly n: number
s: string
}
type I2 = Pick<I1, WritableKeys<I1>>;
// equivalent to { s: string; }
¡Hurra!
Dado que el compilador no verifica minuciosamente readonly
las 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 " readonly
los 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
, protected
y abstract
, pero lo dudaría. Sucede que las propiedades de la clase private
y protected
se 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 private
o protected
puede ser imposible, por la misma razón por la que excluirlas es tan fácil: keyof Foo
no 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.
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
}