Allow stricter structural typing
jbrantly opened this issue · 7 comments
Say I have the following:
interface Options {
doSomethingSpecial?: boolean
}
function configure(options: Options) {}
configure({});
The preceding (correctly) does not flag any issues. However, say I change the function call to:
configure({doSomethingSuper: true});
This also does not flag any issues. I understand that in many (most?) cases this is the desired behavior, but in other cases it seems that a stricter matching (ie, you don't have to specify "doSomethingSpecial" but you can't specify anything else either) would be desirable. There are at least two instances that I can think of where this applies:
- Catching typos
- (more importantly) Compile-time checking after a refactoring of "doSomethingSpecial" to be named something else
Perhaps some new syntax for enabling this mode would be useful. For example:
function configure(options: Options!) {}
We've definitely had this sort of request before, doesn't appear to have been logged on GitHub yet.
The basic idea is to be able to specify a type that says 'you can/must have compatible properties x,y,z but you cannot have any additional properties' where currently TypeScript only allows a contract like 'you can/must have compatible properties x,y,z but you can have anything else too.' This is particularly problematic for options bags. Previous proposals have been along the lines of what you wrote:
interface MyOptions {
isAwesome: boolean;
}
function doWork(options: sealed MyOptions) {}
doWork({ someUnknownProperty: 1 }); // errorAnother alternative is that the type definition itself would be the place you specify this:
interface sealed MyOptions {
theOnlyAllowedProperty: string;
}but then you couldn't use this feature with object type literals, the type could only ever be used in a sealed fashion (maybe this is desirable), and you'd likely have to ensure sealed-ness was always matching between interface definitions. There're also questions of would your sealed-ness be inherited (I think not) and is it applied recursively to the types of your properties (doubtful).
Would be interested to hear whether people prefer it on type declarations, in type position (or both), and why. Are there are common patterns you'd use this for besides options bags?
Note: sealed is just one possible keyword, there're others and depending on the proposal sealed could actually be a bad choice due to the baggage it carries from other languages.
To answer your question about common patterns, I think wrappers around interfaces (eg http://backbonejs.org/#Model-set) would benefit from this. You have a well defined interface for your model and anything outside of that interface is likely wrong.
My preference would be allowing it on type declarations and in type position, but if I had to pick only one I would say in type position.
I agree that this is a common source of errors - especially when refactoring code which results in properties being renamed.
Would be interested to hear whether people prefer it on type declarations, in type position (or both), and why
My preference is for the sealed keyword to go on the type declaration. This fits in with our code-base where we have a single clearly defined object literal (say a "Point" type with x and y properties) that is used in multiple places. Permitting sealed in a type position seems to complicate matters a bit too much, IMO.
See also the discussion "Should object literals with surplus properties be assignable to subtypes?" on the old codeplex site. I don't quite agree that this important piece of functionality should be left to a third party linting tool, as suggested in that discussion.
👍
definitely good and very useful feature suggestion. I had situations where this would find errors at compile time and I believe this what we strive for.
my opinion is the same as jbrantly:
My preference would be allowing it on type declarations and in type position, but if I had to pick only one I would say in type position.
Another example is ReactJS's props objects:
interface ChatCardComponentProps {
isActive?: boolean;
}
render() {
// first argument must be of ChatCardComponentProps type
ChatCardComponent({
isActive: /* some condition */ true
})
}Later component's author can rename interface property from isActive to isSelected and there will be an issue. We can't catch it by compiler now. This is very common case in practice.
👍 to the feature.
ps. I think that interface definition is a wrong place for keyword because this issue is about usage of type in some known context. Type position like function doWork(options: sealed MyOptions) {} is a good place for this.
Some ideas about naming: exact, strict or ! after the type function doWork(options: MyOptions!)
One thing to consider about moving the exact/strict specifier out of the type definition is that if you want the compiler to do the checking then, you'll have to add that specifier everywhere. example:
interface Options {
altKey?: boolean;
shiftKey?: boolean;
ctrlKey?: boolean;
metaKey?: boolean;
}
function simulate(element: HTMLElement, type: string, options: Options!) {
// simulate the event
}
var options: Options!;
options = { shiftKey: false, maltKey: false }; // error
var moreOptions: Options;
options = { shiftKey: false, maltKey: false }; // okay
function simulateMouseDown(element: HTMLElement, options: Options) {
simulate(element, "mousedown", options); // ???
}
If we declare any parameter as exact/strict should you be able to pass it a variables of the same type that haven't been declared exact/strict? Same question with variable assignment.
I think the goal is avoid spelling mistakes when creating object literals. Already you can't add additional properties to objects of a specific interface if that interface doesn't already have it. Since object literals are the thing that's affected by the proposed change, maybe the new syntax should apply to the literal itself, e.g.
var options: Options;
options = {! shiftKey: false, maltKey: false }; // error
simulateMouseDown(document.body, {! shiftKey: false, maltKey: false }); // error
Fixed in #3823.