Búsqueda de sitios web

¿Patrón para parámetros genéricos de TypeScript que a veces pueden ser undefined?


Quiero una jerarquía de clases donde algunas implementaciones de una clase base permitan que un miembro no esté definido, mientras que en otras esto nunca será el caso. He estado tratando de averiguar cómo plantear esto durante todo el día porque he probado algunos enfoques y me he encontrado con diferentes limitaciones del lenguaje según lo que haga. Intentaré resumir el problema con un ejemplo de un enfoque que he probado:

function f(foo: Foo): number { return foo.bar; }

class Foo { bar: number; }

abstract class Base<T extends Foo | undefined> {
    data: T;
    public f(): number {
        if (!this.data) { return -1; }
        return f(this.data); // bad: this.data is still `Foo | undefined` not just `Foo`
    }
}

class Always extends Base<Foo> {
  constructor() { super(); this.data = new Foo(); }
}

class Sometimes extends Base<Foo | undefined> {}

let a = new Always();
let s = new Sometimes();
a.data.bar = 1; // good: no error because `a.data` must not be undefined
s.data.bar = 1; // good: compiler flags this because s.data can be undefined

La línea de arriba que etiqueté como "mala" es el problema, porque aún no puedes acotar un genérico de unión, no hay manera de usar un guard simple para asegurar que data no es undefined. Por supuesto, puedo escribir f(this.data as Foo), pero quiero evitar hacerlo en todas partes donde lo referencio si es posible.

He jugado brevemente con los tipos condicionales agregando un segundo parámetro genérico que extiende boolean, pero tampoco he podido hacerlo funcionar. Básicamente, todas las soluciones que he ideado por mi cuenta rompen una de las 3 líneas con comentarios en el ejemplo: f(this.data), a.data.bar siempre siendo válido, o s.data.bar siempre siendo inválido si aún no has comprobado que s.data está definido.

Respuesta #1

Si estás abierto a usar un segundo parámetro de tipo para representar la posibilidad de indefinido, esto funciona:

function f(foo: Foo): number { return foo.bar; }

class Foo { bar: number; }


abstract class Base<T extends Foo, TUndefiend extends undefined> {
    data!: T | TUndefiend;
    public f(): number {
        if (!this.data) { return -1; }
        return f(this.data); // ok now
    }
}

class Always extends Base<Foo, never> {
  constructor() { super(); this.data = new Foo(); }
}

class Sometimes extends Base<Foo, undefined> {}

let a = new Always();
let s = new Sometimes();
a.data.bar = 1; // good: no error because `a.data` must not be undefined
s.data.bar = 1; // good: compiler flags this because s.data can be undefined

Los guardianes de tipo no se reducirán a un parámetro de tipo, pero si hay dos parámetros de tipo diferentes en una unión, eso parece funcionar bien.

El parámetro de tipo extra (TUndefined) desaparecerá si pasamos never, ya que T | never es T y todo funciona como se espera en la clase derivada Always. Si pasamos undefined, entonces tenemos que verificar si es undefined, nuevamente como se espera en Sometimes