Pesquisa de site

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;
}

Artigos relacionados