Forked from jrheling/typeguard_in_inner_function2.ts
Last active
December 19, 2019 15:30
-
-
Save madsimian/765ddfdbf32aeeaca05007cdd185da5b to your computer and use it in GitHub Desktop.
typescript type guards around closure definitions are ignored by the inner function
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
// Typescript handles type guards fundamentally differently than it | |
// handles data in the context of a closure. | |
// | |
// This is not new, and was documented at | |
// https://github.com/microsoft/TypeScript/issues/7662 | |
// and | |
// https://github.com/microsoft/TypeScript/issues/8552 | |
// | |
// Demonstration follows. | |
interface namedThing { | |
name: string; | |
val: number; | |
} | |
interface myType { | |
p1: namedThing | null; | |
p2: number; | |
} | |
// [jb] | |
// Function that will set the property of an object to null after a | |
// delay. This helps illustrate the difficulty of even a runtime type | |
// guard giving confidence that the captured context of a closure | |
// hasn't been mutated. | |
const delay = 200; | |
const setNullBomb = function (obj: any, key: string) : void { | |
// const nullIt = () => obj[key] = null; | |
// const nullIt = () => delete obj[key]; | |
const nullIt = () => obj[key] = 1; | |
setTimeout(nullIt, delay); | |
} | |
// Given a list of namedThings and an object of myType, return | |
// a function that will itself return true if the object is in the | |
// list. | |
// | |
// Note: the inner function is gratuitous here, and is present only | |
// to demonstrate the behavior of data and type guards across the | |
// closure boundary. | |
function funcFactory(s: namedThing[], o: myType): () => boolean { | |
// this doesn't work because o.p1 could be null | |
//let thingWeAreLookingFor: namedThing = o.p1; | |
// with a type guard, we're good | |
let thingWeAreLookingFor: namedThing; | |
if (o.p1) { | |
thingWeAreLookingFor = o.p1; | |
} | |
// but that same type guarding approach does *not* work | |
// when we're trying to guard the variable within | |
// a closure -- the following fails because the compiler | |
// says that o.p1 might be null, even though it's wrapped | |
// in the type guard that guarantees otherwise. | |
// if (o.p1) { | |
// const innerF = function() { | |
// let foundIt = false; | |
// s.forEach( e => { | |
// if (e.name == o.p1.name) { | |
// foundIt = true; | |
// } | |
// } | |
// ) | |
// return(true); | |
// } | |
// } | |
// if the type guard is moved inside of the inner function, | |
// all is well | |
const innerF = function() { | |
let foundIt = false; | |
s.forEach( e => { | |
if (o.p1) { | |
console.log(`comparing ${e.name} to ${o.p1.name}`); | |
if (e.name == o.p1.name) { | |
foundIt = true; | |
} | |
} | |
}) | |
// [jb] Unbeknownst to the type guard, we can exfiltrate part | |
// of the closure's conteext and do unsafe things with it | |
// later. Because it's a callback happening at runtime, it's | |
// really hard for the compiler to be sure the type guard | |
// isn't exposed like this, even though the guard is also a | |
// runtime check. This bomb is effective anywhere in | |
// funcFactory's scope, including inside the guard. | |
setNullBomb(o, 'p1'); | |
return(foundIt); | |
} | |
return(innerF); | |
} | |
let obj = { | |
p1: { name: "foo", val: 2 }, | |
p2: 2 | |
} | |
let objList = [ | |
{ name: "bar", val: 2 }, | |
{ name: "foo", val: 3 }, | |
{ name: "obazof", val: 4 }, | |
] | |
let myFunc = funcFactory(objList, obj); | |
console.log(`1) The item ${myFunc() ? 'was' : 'was not'} found in the list`); | |
// changing what we're looking for now won't change myFunc, since it's | |
// bound to the values present when the closure was created | |
// [jb] Well, it's bound to the values of the *references* when the | |
// closure is created; because we can capture a reference by enclosing | |
// the same context in a different closure, we can still mutate the | |
// value. | |
// [jb] The next assignment to `obj` doesn't change anything in the | |
// scope of the innerF function, so that function still returns true. | |
// N.B. `innerF`'s closure didn't capture a reference to `obj`, it | |
// captured a reference to `o` inside funcFactory, which took its | |
// value from `obj` when called. | |
// Because no previously enclosed context contains a reference to the | |
// obj identifier, and the assignment here creates a *new* object with a | |
// new p1 value, it doesn't affect myFunc(); | |
obj = { | |
p1: { name: "food", val: 2 }, | |
p2: 2 | |
} | |
console.log(`2) The item ${myFunc() ? 'was' : 'was not'} found in the list`); | |
// To show how a type guard just can't provide assurance of type | |
// safety to a variable in a closure, we wait 100ms longer than the | |
// time bomb's delay, then check on return value of myFunc. | |
const logMe = () => console.log(`3) The item ${myFunc() ? 'was' : 'was not'} found in the list`); | |
setTimeout(logMe, delay + 100); | |
// Obviously the fact that changing obj's value after creating myFunc has no | |
// impact on the value of myFunc is not surprising - this is the core of | |
// makes the closure a closure. | |
// | |
// [jb] in more deeply functional language (say *cough* Clojure | |
// *cough*), closures would capture values, and that would be | |
// that. | |
// | |
// But the point of the more drawn out example is to demonstrate that data and | |
// type guards behave differently across the closure's scope boundary. IOW, | |
// tsc logically *could* let the type guard defined in the outer scope "work" | |
// in the inner scope, it just chooses not to. | |
// [jb] I think type guards work the same way at runtime, it's just | |
// the compiler that has a blind spot when it comes to closures and | |
// their risk to type safety. Normally the compiler sees a type guard | |
// expression, understands the intent, can agree on the intended | |
// safety of the inner block. If the block is a closure, the | |
// possibility of context capture and mutation in another scope that's | |
// not statically analyzable by the compiler means the Typescript team | |
// decided to let a warning stand in the place of this | |
// possibility. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment