Home > Mobile >  Casting an object literal as a function parameter to a type with full type checking in typescript
Casting an object literal as a function parameter to a type with full type checking in typescript

Time:12-30

I have a bunch of JavaScript code someone wrote that I'm converting to typescript. I have lots of code like this

const shapes = [];

shapes.push({ name: 'Circle', radius: 12 });
shapes.push({ name: 'Rectangle', width: 34, height: 56 });
...

I want to make this more type safe so I've added

interface Shape {
   name: string;
}

interface Circle extends Shape {
   radius: number;
}

interface Rectangle extends Shape {
   width: number;
   height: number;
}

Now, I want to fix the code to be more type checked. My first attempt was this

const shapes: Shape[] = [];

shapes.push({ name: 'Circle', radius: 12 } as Circle);
shapes.push({ name: 'Rectangle', width: 34, height: 56 } as Rectangle);
...

The problem is, this as Type syntax is not actually fully type checking. The following line passes even though the required 'height' property is missing

shapes.push({ name: 'Rectangle', width: 34 } as Rectangle);

I can fix it by moving the creation outside the function call

const r: Rectangle = { name: 'Rectangle', width: 34 };  // ERROR: missing height (Yay!)
shapes.push(r);

But, it makes the code really ugly given the long list of shapes.

Is there some other way than as Type to cast an object literal to a type inline as a parameter to a function that will do full type checking?

PS: I'm not looking for a way to refactor the code (like shape constructors/factories). I'm just asking the previous question "Is there some other way than as Type to cast an object literal to a type inline as a parameter to a function that will do full type checking?" My actual code is nothing like this but I needed to make a small example to demo the issue

typescript playground

CodePudding user response:

I'd use a discriminated union instead (but see below, there is an alterative using your current structure); then TypeScript can ensure that the object being provided has the necessary properties based on its name:

interface Circle {
    name: "Circle";
    radius: number;
}

interface Rectangle {
    name: "Rectangle";
    width: number;
    height: number;
}

type Shape = Circle | Rectangle;

const shapes: Shape[] = [];

// These work:
shapes.push({ name: "Circle", radius: 12 });
shapes.push({ name: "Rectangle", width: 34, height: 56 });

// This causes an error: 
shapes.push({ name: "Rectangle", width: 34 });

Playground example

Notice how the relationship between Shape and Circle/Rectangle is inverted: Now we derive Shape from Circle, Rectangle, and (down the line) other shapes. The name property in each of the shapes is a string literal property (it can only have the one value the shape defines for it for any given shape). When we create the union of shapes, name can be used to narrow members to specific shapes.


Is there some other way than as Type to cast an object literal to a type inline as a parameter to a function that will do full type checking?

TypeScript doesn't have casting, it has type assertions. In languages that have casting, a cast can actually modify a value (for instance, you can use a cast to convert the actual bit values of an IEEE-754 floating point double into a 64-bit two's complement int). TypeScript doesn't have that. All that a type assertion does is tell TypeScript that you, the programmer, know that something fulfills the requirements of a given type.

But for what you're describing, TypeScript just added the satisfies operator which tells TypeScript you expect something to satisfy a type contract (without forcing it to). You could use it for this, but it's ugly:

// These work:
shapes.push({ name: "Circle", radius: 12 } satisfies Circle as Shape);
shapes.push({ name: "Rectangle", width: 34, height: 56 } satisfies Rectangle as Shape);

// This causes an error: 
shapes.push({ name: "Rectangle", width: 34 } satisfies Rectangle as Shape);

Playground example

The satisfies triggers the type check. Then we need a type assertion because the objects are defined with inline object literals, triggering TypeScript's excess property checks. (You wouldn't need it if the objects weren't defined inline [example], but doing it inline was one of your requirements.)

  • Related