173

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?

ruffin
  • 16,507
  • 9
  • 88
  • 138
Elias Garcia
  • 6,772
  • 11
  • 34
  • 62

3 Answers3

242

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 = req.body as IToDoDto;
// or
const toDo = <IToDoDto> req.body; // deprecated

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);
milosmns
  • 3,595
  • 4
  • 36
  • 48
Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
  • 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 – huyz Dec 26 '18 at 18:22
  • 23
    for anyone who is looking for type assertion in general, do not use <>. this is deprecated. use ```as``` – Abhishek Deb May 16 '19 at 15:54
  • 2
    "_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](https://en.wikipedia.org/wiki/Syntactic_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 May 18 '20 at 17:39
  • @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. – Nitzan Tomer May 19 '20 at 11:14
  • [Tomayto tomahto](https://idioms.thefreedictionary.com/tomato%3b+tomato)? 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. – ruffin May 19 '20 at 14:30
10

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.

Sepehr
  • 2,051
  • 19
  • 29
10

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.

Jason
  • 2,382
  • 1
  • 16
  • 29
  • 1
    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án Matók Jun 07 '20 at 14:18
  • @ZoltánMatók good point. Also, regarding sending the serialized object over the network there's [an argument](https://stackoverflow.com/questions/49053103/typescript-what-is-better-get-set-properties/49053971#comment103513875_49053971) for Java style getters and setters over the JavaScript `get` and `set` methods. – Jason Jun 08 '20 at 20:38