Skip to content

Instantly share code, notes, and snippets.

@jrheling
Created December 18, 2019 17:47
Show Gist options
  • Save jrheling/149ef2db274ce0f9722fc7322b972cd9 to your computer and use it in GitHub Desktop.
Save jrheling/149ef2db274ce0f9722fc7322b972cd9 to your computer and use it in GitHub Desktop.
typescript type guards around closure definitions are ignored by the inner function
// 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;
}
// 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;
}
}
})
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
obj = {
p1: { name: "food", val: 2 },
p2: 2
}
console.log(`2) The item ${myFunc() ? 'was' : 'was not'} found in the list`);
// 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.
//
// 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.
@jrheling
Copy link
Author

See https://gist.github.com/madsimian/765ddfdbf32aeeaca05007cdd185da5b for a good outline of why this is appropriate

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