ChatGPT解决这个技术问题 Extra ChatGPT

Cast object to interface in TypeScript

I'm trying to make a cast in my code from the body of a request in express (using body-parser middleware) to an interface, but it's not enforcing type safety.

This is my interface:

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

This is the code where I'm trying to do the cast:

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

And finally, the service method that's being called:

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

I can pass whatever arguments, even ones that don't come close to matching the interface definition, and this code will work fine. I would expect, if the cast from response body to interface is not possible, that an exception would be thrown at runtime like Java or C#.

I have read that in TypeScript casting doesn't exist, only Type Assertion, so it will only tell the compiler that an object is of type x, so... Am I wrong? What's the right way to enforce and ensure type safety?

Please define "it's not working". Be precise. Is there an error? Which one? At compile-time? At runtime? What happens?
At runtime, the code executes normally, with whatever object I pass.
It's not clear what you're asking
My question is how to cast the incoming object to an a typed object. If the cast is not possible, throw an exception at runtime, like Java, C#...
Does this answer your question? TypeScript or JavaScript type casting

N
Nitzan Tomer

There's no casting in javascript, so you cannot throw if "casting fails".
Typescript supports casting but that's only for compilation time, and you can do it like this:

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

You can check at runtime if the value is valid and if not throw an error, i.e.:

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

Edit

As @huyz pointed out, there's no need for the type assertion because isToDoDto is a type guard, so this should be enough:

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

this.toDoService.addToDo(req.body);

I don't think that you need the cast in const toDo = req.body as IToDoDto; since the TS compiler knows that it is an IToDoDto at this point
for anyone who is looking for type assertion in general, do not use <>. this is deprecated. use as
"There's no casting in javascript, so you cannot throw if "casting fails"." I think, more to the point, interfaces in TypeScript are not actionable; in fact, they're 100% syntatic sugar. They make it easier to maintain structures conceptually, but have no actual impact on the transpiled code -- which is, imo, insanely confusing/anti-pattern, as OP's question evidences. There's no reason things that fail to match interfaces couldn't throw in transpiled JavaScript; it's a conscious (and poor, imo) choice by TypeScript.
@ruffin interfaces are not syntactic sugar, but they did make a conscious choice to keep it in runtime only. i think it's a great choice, that way there's no performance penalty at runtime.
Tomayto tomahto? The type safety from interfaces in TypeScript doesn't extend to your transpiled code, and even pre-runtime the type safety is severely limited -- as we see in the OP's issue where there is no type safety at all. TS could say, "Hey, wait, your any isn't guaranteed to be IToDoDto yet!", but TS chose not to. If the compiler only catches some type conflicts, and none in the transpiled code (and you're right; I should've been more clear @ that in the original), interfaces are unfortunately, imo, [mostly?] sugar.
S
Sepehr

Here's another way to force a type-cast even between incompatible types and interfaces where TS compiler normally complains:

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

  // ... do runtime checks here

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

Then you can use it to force cast objects to a certain type:

import { forceCast } from './forceCast';

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

Note that I left out the part you are supposed to do runtime checks before casting for the sake of reducing complexity. What I do in my project is compiling all my .d.ts interface files into JSON schemas and using ajv to validate in runtime.


J
Jason

If it helps anyone, I was having an issue where I wanted to treat an object as another type with a similar interface. I attempted the following:

Didn't pass linting

const x = new Obj(a as b);

The linter was complaining that a was missing properties that existed on b. In other words, a had some properties and methods of b, but not all. To work around this, I followed VS Code's suggestion:

Passed linting and testing

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

Note that if your code attempts to call one of the properties that exists on type b that is not implemented on type a, you should realize a runtime fault.


I'm glad I found this answer, but note that if you are sending 'x' over the network or to another app, you could be leaking personal info (if 'a' is a user for example), because 'x' still has all the properties of 'a', they are just unavailable for typescript.
@ZoltánMatók good point. Also, regarding sending the serialized object over the network there's an argument for Java style getters and setters over the JavaScript get and set methods.