Last active
June 27, 2020 16:05
-
-
Save garronej/0a692f5963cf682a851d24a27baf482b to your computer and use it in GitHub Desktop.
How to do safe type casting with TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
Author
garronej
commented
Jun 27, 2020
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment