Operador keyof

Por ser uma linguagem dinâmica, o JavaScript algumas vezes torna difícil a representação de algumas operações em um sistema tipado. Por exemplo, vejamos a seguinte função:

1
2
3
function prop(obj, propertyName) {
return obj[propertyName];
}

Essa função aceita um objeto e um nome de propriedade e retorna o valor correspondente desta propriedade. É importante observar que diferentes propriedades em um objeto possuem diferentes tipos. Com isso em mente, como podemos definir o tipo de retorno desta função?

Vamos a uma primeira tentativa:

1
2
3
function prop(obj: {}, propertyName: string) {
return obj[propertyName];
}

Adicionamos 2 anotações obj: {}, propertyName: string. Com isso estamos dizendo que obj precisa ser um objeto e que propertyName precisa ser do tipo string.

Agora como definir o tipo de retorno desta função?

Vamos a um exemplo prático:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function prop(obj: {}, propertyName: string) {
return obj[propertyName];
}
const myObj = {
strProp: 'str value',
boolProp: true,
numberProp: 100
}
const val1 = prop(myObj, 'strProp'); // tipo de retorno 'any'
const val2 = prop(myObj, 'boolProp'); // tipo de retorno 'any'
const val3 = prop(myObj, 'numberProp'); // tipo de retorno 'any'
const val4 = prop(myObj, 'xyz'); // irá falhar em tempo de execução.
// xyz não é uma propriedade válida neste objeto

Para qualquer propriedade existente que pasemos o retorno será inferido como sendo do tipo any. Para o caso de passar uma propriedade inexistente teremos um erro em tempo de execução.

Isso ocorre por que neste caso o compilador do TypeScript não consegue prever de antemão quais possíveis objetos serão passados como argumento para prop(...) e com isso não é possível inferir o tipo de retorno.

Para fazer com que o compilador reconheça o tipo de obj poderíamos ajustar o código da seguinte forma:

1
2
3
function prop<T>(obj: T, propertyName: string) {
return obj[propertyName];
}

Note que estamos utilizando um tipo genérico que será inferido no momento em que um objeto for passado em obj. Mesmo com esse ajuste, seguimos não alcançando o resultado esperado. Embora agora o compilador entenda o tipo de obj ele continua não sabendo se propertyName realmente existe em obj e, caso exista, qual o seu tipo.

Ok, vamos supor agora em um outro exemplo que sabemos de antemão o tipo exato que será passado para a função prop(...). Suponha que tenhamos uma interface com a seguinte estrutura:

1
2
3
4
5
interface MyInterface {
id: number;
text: string;
due: Date;
}

Para entender melhor o próximo exemplo de código precisamos relembrar outras duas funcionalidades importantes:

  • Union types - Permite informar se um objeto pode ser um ou mais tipos diferentes. Veja mais.
  • String literal - Permite definir quais valores um objeto do tipo string pode assumir. Veja mais.

Vamos reescrever a função da seguinte forma:

1
2
3
function prop(obj: MyInterface, propertyName: "id" | "text" | "due") {
return obj[propertyName];
}

Vejamos o que temos até agora. Com essa mudança tentarmos passar uma propriedade que não existe na interface MyInterface, ou seja, que não esteja no tipo "id" | "text" | "due", teremos um erro em tempo de compilação.

1
const val4 = prop(myObj, 'xyz'); // erro! 'xyz' não pertende ao tipo "id" | "text" | "due"

Isso é bom, resolve parte do problema, mas também nos gera um novo problema. Agora estamos protegendo o valor de propertyName definindo o que realmente pode ser passado mas continuamos não tendo como prever o tipo de obj e as possíveis propriedades de obj em propertyName. Nessa função precisa ser genérica, ou seja, funcionar em todos os casos e não somente para o tipo MyInterface.

Solução

Para resolver este tipo de problema foi acrescentado na versão 2.1 do TypeScript o operador keyof. O objetivo deste operador é retornar um “union type” de “string literals” representando o nome de cada propriedade do tipo em que estamos aplicando o keyof. Confuso não?

Vamos a um exemplo para entender melhor. Utilizando a mesma interface do exemplo anterior, podemos utilizar o operador keyof para retornar um tipo que represente um “union type” de “string literals” com a combinação dos nomes das propriedades de MyInterface:

1
type MyInterfaceKeys = keyof MyInterface; // "id" | "text" | "due"

Isso equivale ao mesmo que escrever:

1
type MyInterfaceKeys = "id" | "text" | "due";

Mas em alguns casos precisamos que isso seja feito de forma dinâmica sendo recuperado direto de uma dada interface e por isso nesses casos utilizaremos o keyof.

Com base no que acabou de ser apresentado, vamos alterar a função prop(...) da seguinte forma:

1
2
3
function prop<T, K extends (keyof T)>(obj: T, propertyName: K) {
return obj[propertyName];
}

OK, vamos entender as modificações parte a parte:

  • Como demonstrado em um exemplo anterior, estamos utilizando um tipo genérico (T) para inferir o tipo de obj em tempo de compilação.
  • Baseado também em exemplos anteriores, estamos utilizando keyof T para recuperar um tipo que represente os possíveis nomes de propriedades de T (repare que estamos colocando keyof entre parêntesis no exemplo só pra facilitar a leitura, no entanto esses parêntesis não são necessários).
  • Estamos definindo um outro tipo genérico K para extender o tipo gerado por keyof T.

Com base nessas informações, agora o compilador consegue inferir que o retorno de prop(...) é T[K], e outras palavras, é o tipo resultante da aplicação da propriedade K no objeto T, e com isso agora temos o retorno da função inferido da forma correta correspondente ao tipo da propriedade solicitada.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function prop<T, K extends (keyof T)>(obj: T, propertyName: K) {
return obj[propertyName];
}
const myObj = {
strProp: 'str value',
boolProp: true,
numberProp: 100
}
const val1 = prop(myObj, 'strProp'); // retorna 'string'
const val2 = prop(myObj, 'boolProp'); // retorna 'boolean'
const val3 = prop(myObj, 'numberProp'); // retorna 'number'
const val4 = prop(myObj, 'xyz'); // irá falhar em tempo de compilação.
// Argument of type '"xyz"' is not assignable to
// parameter of type '"strProp" | "boolProp" | "numberProp"'.

Excelente não? Isso nos ajuda a prevenir uma série de erros. Esta é só uma das formas de aplcação deste operador. Um bom exemplo pode ser encontrado no arquivo lib.es2017.object.d.ts que é instalado junto com o compilador do TypeScript:

1
2
3
4
5
interface ObjectConstructor {
// ...
entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
// ...
}

Essa interface é utilizada para definir o tipo da função Object.entries(), onde o retorno é um array com de pares de valores contendo [propertyName, value] para um dado objeto.

Reference: TS v2.1 https://github.com/Microsoft/TypeScript/pull/11929

Compartilhar Comentarios

TypeScript - Decorators

Fala pessoal! Hoje vou falar sobre uma feature muito legal decorators. Utilizamos decorators para inserir metadados e comportamentos em uma declaração de classe, propriedade, métodos ou parâmetro de uma função. Trata-se de uma função com uma assunatura específica (de acordo com o target).

Para utilizar um decorator precisamos utilizar o simbolo @ junto com o nome do decorator antes do membro do código que estivermos decorando. Exemplo:

1
2
3
4
5
6
class MyClass {
@log
doSomething(arg) {
//...
}
}

NOTA: para utilizar esse recurso é necessário configurar a propriedade experimentalDecorators no arquivo tsconfig.json. Para compilar o arquivo via linha de comando utilize: tsc myFile.ts –target ES5 –emitDecoratorMetadata.

Pontos importantes

  • Os decorators são sempre chamados quando uma classe é declarada e não quando um objeto é instanciado.

  • Multiplos decorators podem ser declarados para um mesmo target.

  • não é permitida a utilização de decorators em construtores.

  • Os decorators podem ser do tipo: ClassDecorator, PropertyDecorator, MethodDecorator ou ParameterDecorator.

Decorators em Métodos

Para definir um decorator para um méroto precisamos criar uma função com os seguintes parâmetros:

  • target - Protótipo da classe que possui o método.

  • propertyKey - Nome do método em que estamos aplicando o decorator. Pode ser um string ou um Symbol

  • descriptor - Uma instância da insterface TypedPropertyDescriptor

No primeiro exemplo eu utilizei um decorator chamado @log. Vamos ver uma possível implementação deste decorator:

1
2
3
4
5
6
7
8
9
10
11
12
13
function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
let originalMethod = descriptor.value; // salvando uma referência para o método original
// NOTE: não use arrou fynction. Utilize uma declaração normal de função para que o contexto `this` seja interpretado corretamente.
descriptor.value = function(...args: any[]) {
console.log("Argumentos da chamada: " + JSON.stringify(args));
let result = originalMethod.apply(this, args); // Executa a função e armazena o resultado
console.log("Valor de retorno: " + result);
return result; // retorna o resultado
};
return descriptor;
}

Chamando a função:

1
2
3
4
5
6
7
8
9
10
class MyClass {
@log
doSomething(arg) {
return "Message -- " + arg;
}
}
new MyClass().doSomething("test");
// => Argumentos da chamada: ["test"]
// => Valor de retorno: Message -- test

Agora que vimos como uma declaração simples de decorator funciona vamos entender como declarar decorators que esperam parâmetros. Para isso precisamos declarar uma função com a assinatura de parâmetros desejado e retornar uma outra função com a mesma assinatura do exemplo anterior. Veja o seguinte código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function log(showArgs: boolean) {
return function(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
let originalMethod = descriptor.value; // salvando uma referência para o método original
// NOTE: não use arrou fynction. Utilize uma declaração normal de função para que o contexto `this` seja interpretado corretamente.
descriptor.value = function(...args: any[]) {
if (showArgs) {
console.log("Argumentos da chamada: " + JSON.stringify(args));
}
let result = originalMethod.apply(this, args); // Executa a função e armazena o resultado
console.log("Valor de retorno: " + result);
return result; // retorna o resultado
};
return descriptor;
}
}

Mais uma vez vamos chamar a função:

1
2
3
4
5
6
7
8
9
class MyClass {
@log(false)
doSomething(arg) {
return "Message -- " + arg;
}
}
new MyClass().doSomething("test");
// => Valor de retorno: Message -- test

NOTA: sempre que utilizarmos uma decorator em uma função estática o target será a propria função ou invés do protótipo da classe.

Decorators em Classes

A assinatura da função que define um decorator para uma classe possui apenas o parâmetro target. Veja o seguinte exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const __myClassDecoratorMetaData: any = {};
function MyClassDecorator(value: string) {
return function (target: Function) {
__myClassDecoratorMetaData[target] = value;
}
}
@MyClassDecorator(“my metadata”)
class MyClass { }
var myClass = new MyClass();
let value: string = __myClassDecoratorMetaData[myClass.constructor];
console.log(value); //=> my metadata

Existe ainda a possibilidade de adicionar decorators em propriedades e argumentos de função. Tentarei cobrir estas outras duas possibilidades em outro artigo.

Até a próxima!

Compartilhar Comentarios

jQuery - 10 dicas de performance

Os anos se passaram e o jQuery continua relevante em pleno 2016. A facilidade de manipulação e a infinidade de artigos na internet ajuda a reforçar a utilização do jQuery e a manter essa biblioteca viva. De fato, poucos anos atrás, escrever codigo compatível com diferentes versões de navegadores seria um trabalho muito árduo se não fosse pelo nosso amigo jQuery.

O grande problema com o jQuery é que muitos desenvolvedores ignoram as boas práticas e escrevem codigo muito pobres em performance (isso sem falar sobre design de código).

Por que performance é importante?

Vou expor duas boas razões:

  • Uma boa performance pode trazer uma boa experiência de uso. Nem sempre seus visitanes vão lhe dizer que seu site está lento, eles simplesmente não vão voltar a acessar o site.
  • O Google leva a performance em consideração para a classificação do seu site no PageRank.

Então, vamos aos pontos…

1. Use a versão mais atual do jQuery sempre que possível

As novas versões sempre trazem melhorias de performance e atualizações de segurança.

2. Utilize os seletores da forma correta

Cada seletor possui uma performance diferente. Organizando do mais rápido para o mais lento temos:

  • Seleção por ID - $('#element-id')
  • Seleção por Elemento - $('form')
  • Seleção por Classe - $('.some-class')
  • Seleção por Pseudo Atributo - $('[data-attr]'), $(':hidden')

3. O algoritmo de seleção do jQuery funciona da esquerda para a direita

Isso significa que o lado direto da query de busca precisa ser o mais específico possivel. Neste caso, queries muito longas acabam não fazendo muito sentido e podem ser evitadas. Exemplo:

Ruim

$('div.page div.block .element')

Bom

$('.page div.element')

Sempre que possível utilize tag + class

4. Quebre sua query em blocos sempre que possível

Seguindo o conceito do item (3) considere os seguintes códigos:

Ruim

$('#container .element')

Bom

$('#container').find('.element')

Lembre-se que a busca é sempre feita da direita para a esquerda e por isso, no primeiro exemplo o jQuery irá primeiro buscar por todos os elementos com a classe element e depois filtrar todos os elementos que contenham o id container.

Uma outra sintaxe similar ao método find(...) é:

$('.element', '#container')

5. Faça sempre um cache dos seus seletores

Este item é muito simular ao anterior. Os exemplos de código abaixo falam por sí só:

Ruim

var block = $('.block');
var elements = $('.block').find('.element');
var title = $('.block').data('title');

Bom

var block = $('.block');
var elements = block.find('.element');
var title = block.data('title');

6. Evite a manipulação pesada do DOM

Você pode se surpreender com a quantidade de elementos que é possível manipular utilizando JavaScript. No entanto, se estes elementos estiverem ligados ao DOM essa surpreza será ruim. Manipular elementos no DOM é uma operação muito lenta e quanto mais elementos você precise manipular, pior será a performance.

A melhor forma de fazer isso utilizando jQuery é primiro “desacoplar” esses elementos no DOM, manipular e depois retorná-los ao DOM. Exemplo:

var elem = $('.element');
var parent = elem.parent();
elem.detach();
... operações muito pesadas, ordenação de tabelas por exemplo ...
parent.append(elem);

7. Evite “appends” desnecessários

Ao invés de ficar utilizando o método append(...) pra cada elemento que estiver adicionando, procure sempre montar uma string HTML e utiliar o append(...) de uma só vez.

8. Prefira utilizar data() no lugar de attr()

O método attr(...) escreve os atributos direto no DOM, e como falamos no item (6), qualquer manipulação direta do DOM deve ser evitada sempre que possível por questões de performance.

NOTA: a utilização de data(...) embora preferível pelas questões citadas, deve ser verificada com cautela. Se voce estiver utilizando uma biblioteca de terceiros que esteja manipulando os atributos direto no DOM você poderá ter problemas.

9. Verifique se o elemento realmente existe antes de usá-lo

Ruim

$('.element').slideDown();
// esse ponto executa uma chamada pesada que efetua vários
// cálculos mesmo se o elemento não existir

Bom

var element = $('.element');
if (element.length) {
    element.slideDown();
}

Verificar se o elemento existe antes de efetuar algumas operações evita que alguns algoritmos sejam executados desnecessáriamente.

10. Evite a utilização de loops

Ruim

$('.element').each(function() {
    $(this).something().somethingElse();
});

Bom

$('.element').something().somethingElse();

A API do jQuery nos permite executar muitas operações em grupo baseado no resultado da query. Utilize as operações em grupo sempre que possível.

Conclusão

jQuery é sem dúvida uma das bibliotecas javascript mais utilizadas e dificilmente perderá esse ranking. Por isso, vale a pena entender melhor como esta biblioteca funciona para utiliza-la da maneira mais correta possível.

Até a próxima!

Compartilhar Comentarios

Arrays tipados - ES6

Os arrays tipados são parte da especificação do ECMAScript 2015 (também conhecido como ES6). Eles foram projetados para facilitar o trabalho com estruturas de dados binárias. Inicialmente os arrays tipados foram introduzidos pelas APIs WebGL com o objetivo de diminuir a dissonância entre as estruturas de dados padrões do JavaScript e as estruturas de dados do C, linguagem base da API WebGL, e com isso permitir ao JavaScript acessar diretamente a estrutura já alocada em memória.

Você pode acessar este link para saber quais browsers já suportam esse recurso: http://caniuse.com/#feat=typedarrays

Criando Arrays tipados

Aqui não existe nenhum segredo, criamos um array tipado da mesma forma que criarpiamos qualquer instância de objeto. em JavaScript:

1
2
3
var arr = new Uint16Array(10);
arr[0] = 0xFFFF;
console.log(arr[0]);

O argumento passado no construtor define o número de elementos que estamos alocando neste array (No exemplo acima estamos alocando 10 elementos do tipo numérico uint16, o que de acordo com a especificação deste tipo de array irá alocar 20 bytes de memória).

Veja abaixo uma lista com os tipos de arrays tipados disponíveis:

Tipo Tamanho (byte) Tipo correspondente em C
Int8Array 1 int8_t
Uint8Array 1 uint8_t
Uint8ClampedArray 1 uint8_t
Int16Array 2 int16_t
Uint16Array 2 uint16_t
Int32Array 4 int32_t
Uint32Array 4 uint32_t
Float32Array 4 float
Float64Array 8 double

ArrayBuffer e DataView

ArrayBuffer e DataView são parte da implementação de arrays tipados. Um ArrayBuffer básicamente armazena os dados de um array tipado. Por exemplo:

1
var fileArrayBuffer = reader.readAsArrayBuffer(file);

Neste código ArrayBuffer ira armazenar todos os bytes do arquivo. Como esse arquivo pode estar em encodes diferentes, precisaremos fazer alguma manipulação para ler seu conteúdo. Por exemplo, se este arquivo for do tipo UTF-8 poderemos associar este ArrayBuffer ao tipo Uint16Array para ler corretamente seu conteúdo.

1
2
var arr = new Uint16Array(fileArrayBuffer);
String.fromCharCode(arr[0]); // A

Já o DataView é um tipo que nos permite acessar estruturas binrias em memória. Exemplo, se tivermos uma estrutura em C declarada da seguinte forma:

1
2
3
4
struct data {
unsigned int id;
char[10] username;
}

Iremos ler esta estrutura em JavaScript com o código:

1
2
3
var buf = new ArrayBuffer(11);
var id = new Uint8Array(buf, 0, 1);
var username = new Uint8Array(buf, 1, 10);

Note que se trata de uma leitura sequencial da memória onde o primeiro byte contém o valor de id e os outros 10 bytes seguintes o valor de username. Aqui estamos utilizando Uint8Array como DataView.

Onde esse recurso ja está sendo utilizado?

  • WebGL - Utiliza typed arrays em buffer, pixels e mapas de testuras
  • Canvas - Canvas utiliza um arrau tipado para armazenar uma imagem.
1
var uint8ClampedArray = ctx.getImageData(...).data;
  • WebSockets - Uma vez habilitado permite a transferência de dados utilizando um ArrayBuffer.
1
webSocket.binaryType = 'arraybuffer';
  • Outras APIs - File API, XMLHttpRequest, Fetch API, window.postMessage() entre outros.

Conclusão

Iremos utilizar os Arrays typados em trechos de código muito específicos onde a performance com a manipulação de estruturas de dados mais complexas for importante, como por exemplo a manipulação de imagens e de som.

É isso, até a próxima.

Compartilhar Comentarios