问题
The question:
Is there any way to write a type annotation in TypeScript that allows any object literal, but doesn't allow built-in types number
, string
, boolean
, Function
or Array
?
Why?
I have been working on improving some type definitions on DefinitelyTyped for some libraries I am using on my projects at work. A common pattern I have noticed in many JS libraries is to pass an 'options' object used to configure the library or plugin. In these cases, you will often you will see type definitions looking something like this:
declare module 'myModule' {
interface IMyModule {
(opts?: IOptions): NodeJS.ReadWriteStream;
}
interface IOptions {
callback?: Function;
readonly?: boolean;
}
}
This allows you to use it like this:
var myModule = require('myModule');
myModule();
myModule({});
myModule({ callback: () => {} });
myModule({ readonly: false });
myModule({ callback: () => {}, readonly: false });
Which all are valid use cases. The trouble is that these type definitions also allow for these non-valid use cases:
myModule('hello');
myModule(false);
myModule(42);
myModule(() => {});
myModule([]);
In many cases, the above calls would result in a runtime error, as the library JS code may try to set default values on the object, or pass the options to another library. Although I have tried many things, I haven't been able to constrain the parameter to accept only objects and not any of those other invalid cases.
It seems that if you have an interface with only optional members (as no one option is required), the compiler will widen acceptable types to accept any
.
You can see a TypeScript Playground demonstration of this problem here: http://bit.ly/1Js7tLr
Update: An example of a solution that doesn't work
interface IOptions {
[name: string]: any;
callback?: Function;
readonly?: boolean;
}
This attempt requires an indexing operator to be present on the object, which means that it now complains about number
s, string
s, boolean
s, Function
s, and Array
s. But this creates a new problem:
var opts = {};
myModule(opts);
This now fails when it should be a valid scenario. (See this in Playground: http://bit.ly/1MPbxfX)
回答1:
For an interface to have sensible enforcement, it must have at least one mandatory member. Essentially, the interface below enforces nothing:
interface IOptions {
callback?: Function;
readonly?: boolean;
}
It is actually equivalent to the "evil interface":
interface IOptions {
}
So you are getting auto-completion, but no real type checking.
Your proposed solution is the way to go...
interface IOptions {
[name: string]: any;
callback?: Function;
readonly?: boolean;
}
In most cases, people tend to construct these options within the function call, but if they really want to do it elsewhere they can use a type assertion:
// If you must
var opts = <IOptions>{};
myModule(opts);
// More typical
myModule({});
// Although... if its empty
myModule();
Disclaimer. A small part of this is just my opinion... but an empty interface is the worst thing you can do with an interface in TypeScript and one that enforces nothing is the second-worst thing.
You could avoid the duplication using your own base interface...
interface Indexed {
[name: string]: any;
}
Or even...
interface Indexed<T> {
[name: string]: T;
}
And then use it on all your options interfaces...
interface IOptions extends Indexed {
callback?: Function;
readonly?: boolean;
}
回答2:
As of TypeScript 2.2, there is now an object
type that does almost exactly what I have described above.
... you can assign anything to the
object
type except forstring
,boolean
,number
,symbol
, and, when usingstrictNullChecks
,null
andundefined
.
Check out the official announcement here: Announcing TypeScript 2.2
来源:https://stackoverflow.com/questions/32187102/typescript-type-annotation-excluding-primitives