typescript - Implementar interface de forma que, quando uma classe a implementar, ela **deva** implementar todas as propriedades da interface, seja como opcionais ou não opcionais
Quero implementar uma interface de forma que, quando uma classe a implementa, ela deve implementar todas as propriedades da interface, sejam elas opcionais ou não opcionais.
Se eu tiver
interface Person {
name: string,
address?: string,
}
class Fisherman implements Person {
name: string;
fishingRodColor: string;
// and any more properties I want
}
O Typescript não vai gerar um erro, tudo bem o Fisherman não implementar address porque é opcional, mas eu quero que ele gere um erro, quero que ele me obrigue a escrever a classe Fisherman como uma dessas duas opções:
class Fisherman implements Person {
name: string;
address?: string;
fishingRodColor: string;
// and any more properties I want
}
ou
class Fisherman implements Person {
name: string;
address: string;
fishingRodColor: string;
// and any more properties I want
}
ambos devem ser permitidos, e pelo menos uma dessas implementações deve ser obrigatória.
Se eu simplesmente escrever minha interface Person assim, com address não sendo opcional
interface Person {
name: string,
address: string,
}
Ele não vai me permitir escrever esta classe, que eu quero que ele me permita:
class Fisherman implements Person {
name: string;
address?: string;
fishingRodColor: string;
// and any more properties I want
}
Porque ele dirá que endereço não pode ser opcional, já que não é opcional na interface Pessoa.
Editar: Agradeço a ajuda, mas outra necessidade que tenho com a solução é que, para o meu caso de uso, no caso em que address seja opcional, eu gostaria de poder declarar este objeto const:
const fisherman: Fisherman: {
name: 'George'
fishingRodColor: 'Red'
}
Mas se address: string | undefined, o TypeScript ainda vai gerar um erro dizendo que fisherman deve ter a propriedade address. O TypeScript quer que eu faça isto:
const fisherman: Fisherman: {
name: 'George'
fishingRodColor: 'Red'
address: undefined
}
Mas eu não quero (e eu acho que também não posso nas novas versões do Mongo, já que undefined foi descontinuado) inserir um campo com undefined no meu banco de dados.
Posso fazer soluções alternativas como deletar undefineds antes de inserir no banco de dados, mas provavelmente isso torna o uso da interface não compensador para este caso de uso.
Resposta #1
Você poderia transformar todas as propriedades opcionais com tipo T em T | undefined com:
type Complete<T> = {
[P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : (T[P] | undefined);
}
Veja https://medium.com/terria/typescript-transforming-optional-properties-to-required-properties-that-may-be-undefined-7482cb4e1585
No seu exemplo:
class FishermanError implements Complete<Person> {
name: string;
fishingRodColor: string;
// error: Property 'address' is missing
}
class Fisherman implements Complete<Person> {
name: string;
fishingRodColor: string;
address: string
// ok
}
class Fisherman2 implements Complete<Person> {
name: string;
fishingRodColor: string;
address: string | undefined
// ok
}
Parque infantil
Resposta #2
Eu não acho que você possa fazer exatamente o que você disse que quer fazer, mas você pode chegar perto: você pode exigir que address seja declarado, mas se o original for address?: string, você acabará tendo que declará-lo como address: string | undefined ou address: string (não address?: string). Isso não é exatamente a mesma coisa que a propriedade não existir, embora geralmente (mas não totalmente) sejam tratadas da mesma maneira. (Dito isso, observe que o TypeScript recentemente recebeu uma flag exactOptionalPropertyTypes relacionada a isso.)
(Editar: Lesiak tem uma versão muito mais concisa do que está abaixo, com uso inteligente de Pick e Required deste artigo.)
A maneira que consigo ver para chegar lá usa dois tipos utilitários e um tipo mapeado com modificadores remapeados. Os tipos utilitários (eu peguei OptionalKeys desta resposta do jcalz, e então modifiquei para obter RequiredKeys):
type OptionalKeys<T> = T extends any
? {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T]
: never;
type RequiredKeys<T> = T extends any
? {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T]
: never;
Isso nos permite descobrir quais chaves são opcionais e quais são obrigatórias. Então podemos usar um tipo mapeado (bem, um par deles combinados) para mudar addres?: string para address: string | undefined:
type NoOptional<T extends object> =
{
[key in RequiredKeys<T>]: T[key];
} &
{
[key in OptionalKeys<T>]-?: T[key] | undefined;
// Removes the optionality −−−−^^ ^^^^^^^^^^^^−−− adds `undefined` to
// the property type
};
Então recebemos um erro aqui como desejado:
class BadFisherman implements NoOptional<Person> { // Error as desired
name: string;
fishingRodColor: string;
// and any more properties I want
constructor() {
this.name = "";
this.fishingRodColor = "";
}
}
mas isso funciona:
class GoodFisherman1 implements NoOptional<Person> {
name: string;
fishingRodColor: string;
address: string | undefined;
// and any more properties I want
constructor() {
this.name = "";
this.fishingRodColor = "";
}
}
e isso funciona:
class GoodFisherman2 implements NoOptional<Person> {
name: string;
fishingRodColor: string;
address: string;
// and any more properties I want
constructor() {
this.name = "";
this.fishingRodColor = "";
this.address = "";
}
}
Link do playground
