typescript - Como posso usar um tipo de enumeração de string como nome de propriedade computada em uma interface?
Estou modelando um formato de solicitação de API em TypeScript:
interface ApiTerm {
term: {
name: string,
value: string,
}
}
interface ApiAnd {
and: {
lhs: ApiItem,
rhs: ApiItem,
}
}
interface ApiOr {
or: {
lhs: ApiItem,
rhs: ApiItem,
}
}
type ApiItem =
| ApiTerm
| ApiAnd
| ApiOr
;
Isso funciona, mas eu precisarei implementar muitas operações binárias além de apenas "e" e "ou", então eu gostaria de alguma forma de encurtar e reutilizar o código.
Seguindo algum outro código que escrevi que usa um valor de enumeração de string específico em uma interface, tentei usar uma enumeração de string como nome da propriedade:
enum Operation {
And = "and",
Or = "or",
}
interface ApiBinary<O extends Operation> {
[O]: {
lhs: ApiItem,
rhs: ApiItem,
}
}
type ApiItem =
| ApiTerm
| ApiBinary<Operation.And>
| ApiBinary<Operation.Or>
;
Infelizmente, isso gera um erro com o TypeScript 2.9.1:
A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
Existe uma solução alternativa que me permita evitar ter que escrever as inúmeras interfaces duplicadas que só diferirão pelo nome da chave?
Olhando para a parte da mensagem de erro "um tipo de 'símbolo único'", não acredito que eu possa criar um enum baseado em Symbols.
Perguntas relacionadas
Usar o valor de enumeração de string em interface TypeScript como uma chave de propriedade computada parece muito próximo, mas trata-se de usar valores específicos de enumeração de string em uma interface, o que funciona como está nas versões mais recentes do TypeScript.
Resposta #1
Em vez de usar uma propriedade computada, você poderia usar um tipo mapeado:
interface ApiTerm {
term: {
name: string,
value: string,
}
}
enum Operation {
And = "and",
Or = "or",
}
type ApiBinary<O extends Operation> = {[o in O]: {
lhs: ApiItem,
rhs: ApiItem,
}
}
type ApiItem =
| ApiTerm
| ApiBinary<Operation.And>
| ApiBinary<Operation.Or>
;
const andExample: ApiBinary<Operation.And> = {
'and': {
lhs: { term: { name: 'a', value: 'b' } },
rhs: { term: { name: 'c', value: 'd' } }
}
}
No entanto, observe que não há como expressar a restrição de que ApiBinary pode ter apenas uma propriedade; por exemplo, alguém poderia declarar type N=ApiBinary<Operation.And | Operation.Or>;
Quando você usa um tipo mapeado, ele não se mistura bem com propriedades computadas - o compilador não consegue inferir que o tipo da propriedade computada está em conformidade com a restrição em ApiItem neste exemplo:
const foo = ({ kind, lhs, rhs }: { kind: Operation, lhs: ApiItem, rhs: ApiItem }): ApiItem =>
({ [kind]: { lhs, rhs } });
O erro diz não é atribuível ao tipo 'ApiBinary<Operation.Or>' porque o compilador tenta verificar a atribuibilidade a todos os membros da união e, se falhar, ele apenas informa sobre o último.
A única maneira que consigo pensar em escrever esta função é longa demais:
const foo = <O extends Operation>({ kind, lhs, rhs }: { kind: O, lhs: ApiItem, rhs: ApiItem }) => {
const result = {} as ApiBinary<O>;
result[kind] = {lhs, rhs};
return result;
}
