Cast objeto para interface em TypeScript

101

Estou tentando fazer uma conversão em meu código do corpo de uma solicitação em expresso (usando middleware analisador de corpo) para uma interface, mas não está impondo a segurança de tipo.

Esta é minha interface:

export interface IToDoDto {
  description: string;
  status: boolean;
};

Este é o código onde estou tentando fazer o elenco:

@Post()
addToDo(@Response() res, @Request() req) {
  const toDo: IToDoDto = <IToDoDto> req.body; // <<< cast here
  this.toDoService.addToDo(toDo);
  return res.status(HttpStatus.CREATED).end();
}

E, finalmente, o método de serviço que está sendo chamado:

public addToDo(toDo: IToDoDto): void {
  toDo.id = this.idCounter;
  this.todos.push(toDo);
  this.idCounter++;
}

Posso passar quaisquer argumentos, mesmo aqueles que não chegam perto de corresponder à definição da interface , e este código funcionará bem. Eu esperaria, se a conversão do corpo da resposta para a interface não fosse possível, que uma exceção fosse lançada no tempo de execução como Java ou C #.

Eu li que na conversão de TypeScript não existe, apenas Type Assertion, então isso só dirá ao compilador que um objeto é do tipo x, então ... Estou errado? Qual é a maneira certa de aplicar e garantir a segurança de tipo?

Elias Garcia
fonte
1
Defina "não está funcionando". Seja preciso. Existe um erro? Qual? Em tempo de compilação? Em tempo de execução? O que acontece?
JB Nizet
1
Em tempo de execução, o código é executado normalmente, com qualquer objeto que eu passar.
Elias Garcia
Não está claro o que você está perguntando
Nitzan Tomer
Minha pergunta é como converter o objeto de entrada para um objeto digitado. Se a conversão não for possível, lance uma exceção em tempo de execução, como Java, C # ...
Elias Garcia
Isso responde sua pergunta? Casting TypeScript ou JavaScript
Michael Freidgeim

Respostas:

143

Não há transmissão em javascript, então você não pode jogar se a "transmissão falhar".
O texto digitado oferece suporte à conversão, mas isso é apenas para o tempo de compilação, e você pode fazer assim:

const toDo = <IToDoDto> req.body;
// or
const toDo = req.body as IToDoDto;

Você pode verificar em tempo de execução se o valor é válido e se não lançar um erro, ou seja:

function isToDoDto(obj: any): obj is IToDoDto {
    return typeof obj.description === "string" && typeof obj.status === "boolean";
}

@Post()
addToDo(@Response() res, @Request() req) {
    if (!isToDoDto(req.body)) {
        throw new Error("invalid request");
    }

    const toDo = req.body as IToDoDto;
    this.toDoService.addToDo(toDo);
    return res.status(HttpStatus.CREATED).end();
}

Editar

Como @huyz apontou, não há necessidade de asserção de tipo porque isToDoDtoé uma proteção de tipo, então isso deve ser suficiente:

if (!isToDoDto(req.body)) {
    throw new Error("invalid request");
}

this.toDoService.addToDo(req.body);
Nitzan Tomer
fonte
Não acho que você precise do elenco, const toDo = req.body as IToDoDto;já que o compilador TS sabe que é um IToDoDtoneste ponto
huyz
10
para quem está procurando por tipo de asserção em geral, não use <>. isto está obsoleto. usaras
Abhishek Deb
Não há conversão em javascript, então você não pode lançar se a“ conversão falhar ”. “ Eu acho, indo direto ao ponto, interfaces em TypeScript não são acionáveis; na verdade, eles são 100% açúcar sintático . Eles tornam mais fácil manter as estruturas conceitualmente , mas não têm impacto real no código transpilado - que é, imo, insanamente confuso / anti-padrão, como a pergunta do OP evidencia. Não há razão para que coisas que falham em corresponder às interfaces não possam gerar JavaScript transpilado; é uma escolha consciente (e pobre, imo) do TypeScript.
ruffin de
As interfaces do @ruffin não são sintáticas, mas fizeram uma escolha consciente de mantê-las apenas em tempo de execução. Acho que é uma ótima escolha, dessa forma não há penalidade de desempenho em tempo de execução.
Nitzan Tomer
Tomayto tomahto ? A segurança de tipo das interfaces no TypeScript não se estende ao seu código transpilado e, mesmo antes do tempo de execução, a segurança de tipo é severamente limitada - como vemos no problema do OP, onde não há segurança de tipo . TS poderia dizer, "Ei, espere, você anyainda não tem garantia de ser IToDoDto!", Mas TS escolheu não fazer. Se o compilador detecta apenas alguns conflitos de tipo, e nenhum no código transpilado (e você está certo; eu deveria ter sido mais claro @ do que no original), as interfaces são, infelizmente, imo, [principalmente?] Açucaradas.
ruffin de
7

Esta é outra maneira de forçar uma conversão de tipo, mesmo entre tipos e interfaces incompatíveis onde o compilador TS normalmente reclama:

export function forceCast<T>(input: any): T {

  // ... do runtime checks here

  // @ts-ignore <-- forces TS compiler to compile this as-is
  return input;
}

Em seguida, você pode usá-lo para forçar a projeção de objetos para um determinado tipo:

import { forceCast } from './forceCast';

const randomObject: any = {};
const typedObject = forceCast<IToDoDto>(randomObject);

Observe que deixei de fora a parte que você deve fazer as verificações de tempo de execução antes do lançamento, para reduzir a complexidade. O que faço em meu projeto é compilar todos os meus .d.tsarquivos de interface em esquemas JSON e usá ajv-los para validar em tempo de execução.

Sepehr
fonte
2

Se isso ajudar alguém, eu estava tendo um problema em que queria tratar um objeto como outro tipo com uma interface semelhante. Tentei o seguinte:

Não passou fiapos

const x = new Obj(a as b);

O linter reclamava que afaltavam propriedades que existiam em b. Em outras palavras, atinha algumas propriedades e métodos de b, mas não todos. Para contornar isso, segui a sugestão do VS Code:

Teste e lint aprovado

const x = new Obj(a as unknown as b);

Observe que se o seu código tentar chamar uma das propriedades que existe no tipo bque não está implementado no tipo a, você deve perceber uma falha de tempo de execução.

Jason
fonte
1
Estou feliz por ter encontrado essa resposta, mas observe que se você estiver enviando 'x' pela rede ou para outro aplicativo, pode estar vazando informações pessoais (se 'a' for um usuário, por exemplo), porque 'x' ainda tem todas as propriedades de 'a', elas estão apenas indisponíveis para texto digitado.
Zoltán Matók
@ ZoltánMatók bom ponto. Além disso, em relação ao envio do objeto serializado pela rede, há um argumento para getters e setters de estilo Java sobre os métodos gete JavaScript set.
Jason