¿Cuáles son las diferencias entre la palabra clave privada y los campos privados en TypeScript?

Respuestas:

43

Palabra clave privada

La palabra clave privada en TypeScript es una anotación en tiempo de compilación . Le dice al compilador que una propiedad solo debe ser accesible dentro de esa clase:

class PrivateKeywordClass {
    private value = 1;
}

const obj = new PrivateKeywordClass();
obj.value // compiler error: Property 'value' is private and only accessible within class 'PrivateKeywordClass'.

Sin embargo, la verificación del tiempo de compilación se puede omitir fácilmente, por ejemplo, desechando la información de tipo:

const obj = new PrivateKeywordClass();
(obj as any).value // no compile error

La privatepalabra clave tampoco se aplica en tiempo de ejecución

JavaScript emitido

Al compilar TypeScript en JavaScript, la privatepalabra clave simplemente se elimina:

class PrivateKeywordClass {
    private value = 1;
}

Se convierte en:

class PrivateKeywordClass {
    constructor() {
        this.value = 1;
    }
}

A partir de esto, puede ver por qué la privatepalabra clave no ofrece ninguna protección de tiempo de ejecución: en el JavaScript generado es solo una propiedad JavaScript normal.

Campos privados

Los campos privados aseguran que las propiedades se mantengan privadas en tiempo de ejecución :

class PrivateFieldClass {
    #value = 1;

    getValue() { return this.#value; }
}

const obj = new PrivateFieldClass();

// You can't access '#value' outside of class like this
obj.value === undefined // This is not the field you are looking for.
obj.getValue() === 1 // But the class itself can access the private field!

// Meanwhile, using a private field outside a class is a runtime syntax error:
obj.#value

// While trying to access the private fields of another class is 
// a runtime type error:
class Other {
    #value;

    getValue(obj) {
        return obj.#value // TypeError: Read of private field #value from an object which did not contain the field
    }
}

new Other().getValue(new PrivateKeywordClass());

TypeScript también generará un error de tiempo de compilación si intenta usar un campo privado fuera de una clase:

Error al acceder a un campo privado

Los campos privados provienen de una propuesta de JavaScript y también funcionan en JavaScript normal.

JavaScript emitido

Si usa campos privados en TypeScript y está apuntando a versiones anteriores de JavaScript para su salida, como es6o es2018, TypeScript intentará generar código que emule el comportamiento en tiempo de ejecución de los campos privados

class PrivateFieldClass {
    constructor() {
        _x.set(this, 1);
    }
}
_x = new WeakMap();

Si está apuntando esnext, TypeScript emitirá el campo privado:

class PrivateFieldClass {
    constructor() {
        this.#x = 1;
    }
    #x;
}

¿Cuál debo usar?

Depende de lo que intentes lograr.

La privatepalabra clave es un buen valor predeterminado. Cumple lo que fue diseñado para lograr y ha sido utilizado con éxito por los desarrolladores de TypeScript durante años. Y si tiene una base de código existente, no necesita cambiar todo su código para usar campos privados. Esto es especialmente cierto si no está apuntando esnext, ya que el JS que TS emite para campos privados puede tener un impacto en el rendimiento. También tenga en cuenta que los campos privados tienen otras diferencias sutiles pero importantes de la privatepalabra clave

Sin embargo, si necesita exigir privacidad en tiempo de ejecución o está generando esnextJavaScript, debe utilizar campos privados.

También tenga en cuenta que las convenciones de organización / comunidad sobre el uso de uno u otro también evolucionarán a medida que los campos privados se generalicen dentro de los ecosistemas JavaScript / TypeScript

Otras diferencias de nota

  • Los campos privados no son devueltos por Object.getOwnPropertyNamesmétodos similares.

  • Los campos privados no son serializados por JSON.stringify

  • Hay casos importantes de importancia en torno a la herencia.

    TypeScript, por ejemplo, prohíbe declarar una propiedad privada en una subclase con el mismo nombre que una propiedad privada en la superclase.

    class Base {
        private value = 1;
    }
    
    class Sub extends Base {
        private value = 2; // Compile error:
    }

    Esto no es cierto con los campos privados:

    class Base {
        #value = 1;
    }
    
    class Sub extends Base {
        #value = 2; // Not an error
    }
  • Una privatepropiedad privada de palabra clave sin un inicializador no generará una declaración de propiedad en el JavaScript emitido:

    class PrivateKeywordClass {
        private value?: string;
        getValue() { return this.value; }
    }

    Compila a:

    class PrivateKeywordClass {
        getValue() { return this.value; }
    }

    Mientras que los campos privados siempre generan una declaración de propiedad:

    class PrivateKeywordClass {
        #value?: string;
        getValue() { return this.#value; }
    }

    Compila a (cuando apunta esnext):

    class PrivateKeywordClass {
        #value;
        getValue() { return this.#value; }
    }

Otras lecturas:

Matt Bierner
fuente
4

Casos de uso: #-campos privados

Prefacio:

Tiempo de compilación y privacidad en tiempo de ejecución

#Privadas campos proporcionan el tiempo de compilación y privacidad en tiempo de ejecución, lo que no es "piratear". Es un mecanismo para evitar el acceso a un miembro desde fuera del cuerpo de la clase de manera directa .

class A {
    #a: number;
    constructor(a: number) {
        this.#a = a;
    }
}

let foo: A = new A(42);
foo.#a; // error, not allowed outside class bodies
(foo as any).#bar; // still nope.

Herencia de clase segura

#-los campos privados obtienen un alcance único. Las jerarquías de clase se pueden implementar sin sobrescribir accidentalmente propiedades privadas con nombres iguales.

class A { 
    #a = "a";
    fnA() { return this.#a; }
}

class B extends A {
    #a = "b"; 
    fnB() { return this.#a; }
}

const b = new B();
b.fnA(); // returns "a" ; unique property #a in A is still retained
b.fnB(); // returns "b"

Afortunadamente, el compilador de TS emite un error cuando las privatepropiedades están en peligro de sobrescribirse (consulte este ejemplo ). Pero debido a la naturaleza de una función de tiempo de compilación, todo es posible en tiempo de ejecución, dado que los errores de compilación son ignorados y / o se utiliza el código JS emitido.

Bibliotecas externas

Los autores de la biblioteca pueden refactorizar # identificadores privados sin causar un cambio importante para los clientes. Los usuarios de la biblioteca en el otro lado están protegidos de acceder a los campos internos.

La API de JS omite # campos privados

Las funciones y métodos JS incorporados ignoran los #campos privados. Esto puede dar como resultado una selección de propiedades más predecible en tiempo de ejecución. Ejemplos: Object.keys, Object.entries, JSON.stringify, for..inbucle y otros ( ejemplo de código ; véase también de Matt Bierner respuesta ):

class Foo {
    #bar = 42;
    baz = "huhu";
}

Object.keys(new Foo()); // [ "baz" ]

Casos de uso: private palabra clave

Prefacio:

Acceso a la clase interna API y estado (privacidad solo en tiempo de compilación)

privateLos miembros de una clase son propiedades convencionales en tiempo de ejecución. Podemos utilizar esta flexibilidad para acceder a la API interna de la clase o al estado desde el exterior. Para satisfacer las comprobaciones del compilador, mecanismos como aserciones de tipo, acceso a propiedades dinámicas o@ts-ignore se pueden usar entre otros.

Ejemplo con aserción de tipo ( as/ <>) y anyasignación de variable con tipo :

class A { 
    constructor(private a: number) { }
}

const a = new A(10);
a.a; // TS compile error
(a as any).a; // works
const casted: any = a; casted.a // works

TS incluso permite el acceso a propiedades dinámicas de un privatemiembro con una escotilla de escape :

class C {
  private foo = 10;
}

const res = new C()["foo"]; // 10, res has type number

¿Dónde puede tener sentido el acceso privado? (1) pruebas unitarias, (2) situaciones de depuración / registro u (3) otros escenarios de casos avanzados con clases internas del proyecto (lista abierta).

El acceso a las variables internas es un poco contradictorio; de lo contrario, no las habría hecho private en primer lugar. Para dar un ejemplo, se supone que las pruebas unitarias son cuadros negros / grises con campos privados ocultos como detalles de implementación. Sin embargo, en la práctica, puede haber enfoques válidos de un caso a otro.

Disponible en todos los entornos ES

Los privatemodificadores TS se pueden usar con todos los objetivos ES. #-los campos privados solo están disponibles para target ES2015/ ES6o superior. En ES6 +, WeakMapse usa internamente como implementación de nivel inferior (ver aquí ). Los #campos nativos- privados actualmente requierentarget esnext .

Consistencia y compatibilidad

Los equipos pueden usar pautas de codificación y reglas de interfaz para imponer el uso privatecomo el único modificador de acceso. Esta restricción puede ayudar con la coherencia y evitar confusiones con la #notación de campo privado de una manera compatible con versiones anteriores.

Si es necesario, las propiedades de los parámetros (abreviatura de asignación de constructor) son un show stopper. Solo se pueden usar con privatepalabras clave y todavía no hay planes para implementarlas para# campos privados.

Otras razones

  • privatepodría proporcionar un mejor rendimiento en tiempo de ejecución en algunos casos de reducción de nivel (ver aquí ).
  • No hay métodos de clase privada disponibles en TS hasta ahora.
  • Algunas personas privateprefieren la notación de palabras clave 😊.

Nota sobre ambos

Ambos enfoques crean algún tipo de tipo nominal o de marca en tiempo de compilación.

class A1 { private a = 0; }
class A2 { private a = 42; }

const a: A1 = new A2(); 
// error: "separate declarations of a private property 'a'"
// same with hard private fields

Además, ambos permiten el acceso entre instancias: una instancia de clase Apuede acceder a miembros privados de otras Ainstancias:

class A {
    private a = 0;
    method(arg: A) {
        console.log(arg.a); // works
    }
}

Fuentes

ford04
fuente