TypeScript: deleting a prop doesn't invalidate the interface

TypeScript Version: 2.1? (Playground)

Code

interface Props {
    foo: string;
    bar: string;
}

const p: Props = {
    foo: 'hello',
    bar: 'world'
}

const say = (props: Props) => console.log(`${props.foo.length} ${props.bar.length}`);

say(p);

delete p.bar;
say(p); // works, but should throw

See here.

Expected behavior:

Don’t compile, because we delete bar from p.

Actual behavior:

Compiles. Resulting in a runtime error: Uncaught TypeError: Cannot read property 'length' of undefined.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 25
  • Comments: 33 (9 by maintainers)

Most upvoted comments

for this case, with --strictNullChecks:

const p = {
    foo: 'hello',
    bar: 'world'
};
delete p.bar;
say(p);

I do not think control flow analysis is the right tool here, but we could disallow deleting a property whose type does not include undefined; treeing it the same way as p.bar = undefined;.

I think treating delete p.bar as p.bar = undefined is the right approach since they both do the same thing.

No, they have different semantics.

var x = {}
'y' in x // false
x.y = undefined
'y' in x // true
delete x.y
'y' in x // false

TypeScript currently lets you delete properties from objects even if they are non-optional, and even if you have every kind of option on like --strictNullChecks, --noImplicitAny, or whatever.

interface Foo {
    y: number
}

const x: Foo = { y: 3 }

delete x.y
// Now x is no longer a 'Foo', with no warnings or errors.

I’d class that as a bug.

I think treating delete p.bar as p.bar = undefined is the right approach since they both do the same thing.

TypeScript currently lets you delete properties from objects even if they are non-optional, and even if you have every kind of option on like --strictNullChecks, --noImplicitAny, or whatever.

i agree. delete should no be allowed under --stricitNullChecks if the property does not include undefined (the type system does not make a distinction between properties that are optional and those that are possibly undefined).

TypeScript currently lets you delete properties from objects even if they are non-optional, and even if you have every kind of option on like --strictNullChecks, --noImplicitAny, or whatever.

This behavior surprised me, too.

+1 I didn’t want to say just +1 but, I definitely think the only thing I can contribute to this is +1. I have already given it a thumbs up, but I have very little confidence that thumbs upping the OP does anything in the eyes of anyone in charge. Hence my extremely long explanation of why I am making a +1 post.

I ran into this today and was surprised:

const myObject = {
  myProperty: 'content',
  myOtherProperty: 'other content',
};

delete myObject.myProperty;

myObject.myProperty.padStart(2); // => Runtime error

This could be easily fixed like this:

type AnOptionalNumber { a?: number; }
const obj: ANumber = { a: 42 };
delete (obj as AnOptionalNumber).a;
obj.a = 42;
printNumber(obj.a);

That is if you know what you’re doing you’re welcome to shoot yourself in the foot. But by default, TypeScript should aim to prevent runtime errors, not tolerate some temporary untyped state of a variable.

The best way I found to remove a prop from the object and infer a new object type is to use es6 destruct and rest features:

interface Props {
    foo: string;
    bar: string;
}

const p: Props = {
    foo: 'hello',
    bar: 'world'
}

const say = (props: Props) => console.log(`${props.foo.length} ${props.bar.length}`);

say(p);

const { bar, ...newP } = p;
say(newP);

But I agree delete on a non optional property should be forbidden.

If such property isn’t optional I don’t think you should be able to delete it, if you need to change the value just assign the new value. Delete is “the same” of assign undefined to the property.

If I was using an IDE Id expect a wavy line under the argument of printNumber l8. Not at l7 where the deletion is operated.

Because this

// snip
const obj: ANumber = { a: 42 };
delete obj.a;
obj.a = 42;
printNumber(obj.a);

should be allowed.

Another example:

interface IMyObject {
  value1: number;
}

const myObject: IMyObject = {
  value1: 5
}

delete myObject.value1;

console.log(myObject.value1); // undefined
console.log(myObject.value1 === undefined); // true
myObject.value1 = undefined; // Type 'undefined' is not assignable to type 'number'.

http://www.typescriptlang.org/play/index.html#code/JYOwLgpgTgZghgYwgAgJIFkCeB5ARgKwgTGQG8AoZZANzgBsBXCARgC5kQGBbXaAbnIBfcuQQB7EAGcSXHASJh2GOYWLIAvGUo16TNsgCsQkQBMIdCJGSy8qsADpajFgNETJYi-bpiA5gAobeWJHXRYASj5kAHpo5AYQMxhQCBM3KU8Ibz9AlQVQ52YNdU0EpJSTSJi4sCgmciC7Ar0NeMSIZJBUqNjkABVMAAcUAHIyjoqR5GBJDjESOElJYF8QOFwLZDAxLaHRzh5oEfsgA

I’m really surprised this bug hasn’t been picked up in over 2 years. @ryanberckmans what feedback do you need for this issue?

To track effects of delete you need to compile with --strictNullChecks and declare the property optional. Upon seeing delete p.bar the control flow analyzer will then consider p.bar to have the value undefined.