Skip to content

Instantly share code, notes, and snippets.

@garronej
Last active June 27, 2020 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save garronej/0a692f5963cf682a851d24a27baf482b to your computer and use it in GitHub Desktop.
Save garronej/0a692f5963cf682a851d24a27baf482b to your computer and use it in GitHub Desktop.
How to do safe type casting with TypeScript
type Shape = Shape.Circle | Shape.Square;
namespace Shape {
export type Circle = {
type: "CIRCLE";
radius: number;
};
export type Square = {
type: "SHARE";
sideLength: number;
};
}
/*
PROBLEM WE ARE TRYING TO ADDRESS:
We have an object of type Shape and we know for sure
that it is a Circle and not a Square.
What is the best way to tell TypeScript that that
our shape object is a sphere?
This is a current problem that we false for example
when we select an element by class name from the DOM.
TypeScript doesn't know exactly what type of element
it is but we do.
We are going to see how we can use:
- Assertion functions: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions
- User defined type guards: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
To solve this problem without having to use "as Circle" and/or
creating a new variable.
*/
//typeGuard and assert are two util function that we are going to leverage in this examples.
export function typeGuard<T>(o: any, isMatched: boolean = true): o is T {
o; //NOTE: Just to avoid unused variable;
return isMatched;
}
export function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg);
}
}
function getCircle(): Shape {
return { "type": "CIRCLE", "radius": 33 };
}
{
/*
We know for sure that our shape object is of type
Shape.Circle but we can't access the "radius" property
because typescript know that it's a shape but don't know
what kink of shape in particular.
*/
const shape: Shape = getCircle();
const radius1 = (shape as Shape.Circle).radius;
const radius2 = (shape as Shape.Circle).radius;
console.log({ radius1, radius2 });
}
//Here we declare a temporary variable and we don't want that!
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
const shape_ = shape as Shape.Circle;
const radius1 = shape_.radius;
const radius2 = shape_.radius;
console.log({ radius1, radius2 });
}
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
if (shape.type === "CIRCLE") {
const radius1 = shape.radius;
const radius2 = shape.radius;
console.log({ radius1, radius2 });
}
}
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
/*
This is a nice approach but there is not always a propriety
we can check on our object that allow us to check the specific type.
*/
assert(shape.type === "CIRCLE");
const radius1 = shape.radius;
const radius2 = shape.radius;
console.log({ radius1, radius2 });
}
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
/*
Here the if statement's argument always return true.
Inside and inside the block shape is a Circle.
This is not a better approach than the
if (shape.type === "CIRCLE") {}
but it show you how the typeGuard function works.
*/
if (typeGuard<Shape.Circle>(shape)) {
const radius1 = shape.radius;
const radius2 = shape.radius;
console.log({ radius1, radius2 });
}
}
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
/*
By negating the typeGuard we can tell typescript to assert
that the shape is not a Square. It will be enough information
for it to assert that shape is a Circle.
*/
assert(!typeGuard<Shape.Square>(shape, false));
const radius1 = shape.radius;
const radius2 = shape.radius;
console.log({ radius1, radius2 });
}
/*
To go further:
The problem is this work also if Shape.Square is not a subtype of Shape.
To cope with that we can use a variant of the typeGuard function
that is more verbose but is able to prevent us from shotting ourself in the
foot sometimes.
*/
//Problem with the current implementation of typeGuard
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
/*
Here TypeScript should be able to tell that
it is impossible for a Shape to be a string.
Yet we hav no type error, after this statement shape
is considered a 'string'
*/
assert(typeGuard<string>(shape));
console.log(shape.toUpperCase());
}
{
/*
Alternative typeGuard that enforce that the type argument provided
is a subtype of the type of 'o'
We would like to default the second type argument to be 'typeof o'
but it is not allowed by the language yet.
See: https://github.com/microsoft/TypeScript/issues/37593 Duplicate see
*/
const typeGuard = <U extends T, T>(o: T): o is U => true;
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
//This still works.
assert(typeGuard<Shape.Circle, typeof shape>(shape));
const radius1 = shape.radius;
const radius2 = shape.radius;
console.log({ radius1, radius2 });
}
{
const shape: Shape = { "type": "CIRCLE", "radius": 33 } as any;
/*
But here we got the expected TS error:
"string" does not satisfy the constraint Shape
*/
assert(typeGuard<string, typeof shape>(shape));
}
}
@garronej
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment