A palavra-chave extends no TypeScript é uma espécie de canivete suíço. É usado em vários contextos, incluindo herança, genéricos e tipos condicionais. Compreender como usar extensões de maneira eficaz pode levar a um código mais robusto, reutilizável e de tipo seguro.
Herança usando extensões
Um dos principais usos de extensões é na herança, permitindo criar novas interfaces ou classes que se baseiam nas existentes.
interface User {
firstName: string;
lastName: string;
email: string;
}
interface StaffUser extends User {
roles: string[];
department: string;
}
const regularUser: User = {
firstName: "John",
lastName: "Doe",
email: "[email protected]"
};
const staffMember: StaffUser = {
firstName: "Jane",
lastName: "Smith",
email: "[email protected]",
roles: ["Manager", "Developer"],
department: "Engineering"
};
Neste exemplo, StaffUser estende User, herdando todas as suas propriedades e adicionando novas. Isso nos permite criar tipos mais específicos com base em tipos mais gerais.
Herança de classe
A palavra-chave extends também é usada para herança de classe:
class Animal {
constructor(public name: string) {}
makeSound(): void {
console.log("Some generic animal sound");
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
makeSound(): void {
console.log("Woof! Woof!");
}
fetch(): void {
console.log(`${this.name} is fetching the ball!`);
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
myDog.makeSound(); // Output: Woof! Woof!
myDog.fetch(); // Output: Buddy is fetching the ball!
Aqui, Dog estende Animal, herdando suas propriedades e métodos, e também adicionando seus próprios.
Restrições de tipo em genéricos
A palavra-chave extends é crucial ao trabalhar com genéricos, permitindo-nos restringir os tipos que podem ser usados com uma função ou classe genérica.
interface Printable {
print(): void;
}
function printObject(obj: T) {
obj.print();
}
class Book implements Printable {
print() {
console.log("Printing a book.");
}
}
class Magazine implements Printable {
print() {
console.log("Printing a magazine.");
}
}
const myBook = new Book();
const myMagazine = new Magazine();
printObject(myBook); // Output: Printing a book.
printObject(myMagazine); // Output: Printing a magazine.
// printObject(42); // Error, number doesn't have a 'print' method
-
interface Printable: Aqui, definimos uma interface chamada Printable. Esta interface declara um contrato que qualquer classe que a implemente deve aderir. O contrato especifica que qualquer classe que implemente Printable deve fornecer um método chamado print que não receba argumentos e retorne void
-
function printObject(obj: T): Esta é uma função genérica chamada printObject. É necessário um único argumento chamado obj, que é o tipo T. O parâmetro de tipo T é restrito a tipos que estendem (implementam) a interface Printable que pode ser usada como argumento para esta função.
a classe Book implementa Printable e a classe Magazine implementa Printable: Aqui, definimos duas classes, Book e Magazine, ambas implementando a interface Printable. Isso significa que essas classes devem fornecer um método de impressão conforme exigido pelo contrato da interface Printable.-
const meuLivro = new Livro(); e const myMagazine = new Magazine();: Criamos instâncias das classes Book e Magazine.-
printObject(meuLivro); e printObject(myMagazine);: Chamamos a função printObject com as instâncias de Book e Magazine. Como as classes Book e Magazine implementam a interface Printable, elas atendem à restrição do parâmetro de tipo T extends Printable. Dentro da função, o método print da classe apropriada é chamado, resultando na saída esperada.-
// printObject(42);: Se tentarmos chamar printObject com um tipo que não implementa a interface Printable, como o número 42, o TypeScript gerará um erro. Isso ocorre porque a restrição de tipo não é satisfeita, pois o número não possui um método de impressão conforme exigido pela interface Printable.-
Em resumo, a palavra-chave extends no contexto da função printObject
(obj: T) é usada para garantir que o tipo T usado como argumento esteja de acordo com o contrato definido pela interface Printable. Isso garante que apenas tipos com um método print possam ser usados com a função printObject, impondo um comportamento e um contrato específicos para o uso da função.
Tipos Condicionais
T estende U ? X: S
T extends U ? X : Y
T é o tipo que está sendo verificado-
U é o tipo de condição contra o qual T está sendo verificado.-
X é o tipo que o tipo condicional avalia se T estende (é atribuível a) U
-
Y é o tipo que o tipo condicional avalia se T não estender U
-
type ExtractNumber = T estende o número? T: nunca;
digite NumberOrNever = ExtractNumber; // número
digite StringOrNever = ExtractNumber; // nunca
T extends U ? X : Y
Aqui, o tipo ExtractNumber usa um parâmetro de tipo T. O tipo condicional verifica se T estende o tipo de número. se isso acontecer, o tipo será resolvido para T (que é o tipo numérico). Caso contrário, o tipo será resolvido como nunca.
A palavra-chave estende com tipos de união
Agora, vamos considerar a expressão A | B | C estende A. Isso pode parecer contra-intuitivo a princípio, mas no TypeScript, essa condição é na verdade falsa. Eis o porquê:
No TypeScript, quando você usa extensões com um tipo de união no lado esquerdo, é equivalente a perguntar: - "Todos os tipos possíveis nesta união podem ser atribuídos ao tipo à direita?"
Em outras palavras, A | B | C estende A está perguntando: - "A pode ser atribuído a A, E B pode ser atribuído a A, E C pode ser atribuído a A?"
Embora A possa certamente ser atribuído a A, B e C podem não ser atribuíveis a A (a menos que sejam subtipos de A), portanto o resultado geral é falso.
-
tipo Fruta = "maçã" | "banana" | "cereja";
digite CitrusFruit = "limão" | "laranja";
type IsCitrus = T estende CitrusFruit? verdadeiro: falso;
digite Teste1 = IsCitrus; // verdadeiro
digite Test2 = IsCitrus; // falso
digite Test3 = IsCitrus; // falso
T extends U ? X : Y
Neste exemplo, IsCitrus é falso porque nem todas as frutas na união Fruit são CitrusFruit.
Melhores práticas e dicas
- Use extensões para relacionamentos significativos: use herança apenas quando houver um relacionamento "é-um" claro entre os tipos.
- Prefira composição em vez de herança: Em muitos casos, a composição (usando interfaces e interseções de tipos) pode ser mais flexível do que a herança de classes.
- Seja cauteloso com cadeias de herança profundas: a herança profunda pode tornar o código mais difícil de entender e manter.
- Aproveite tipos condicionais para APIs flexíveis: use tipos condicionais com extensões para criar APIs que se adaptam com base em tipos de entrada.
- Use extensões em genéricos para criar funções reutilizáveis e com segurança de tipo: Isso permite que você escreva funções que funcionam com uma variedade de tipos, mantendo a segurança de tipo