How do you type an object that can have both a few declared optional properties, e.g.:
{
hello?: string,
moo?: boolean
}
as well as custom properties (that must be functions), e.g.:
[custom: string]: (v?: any) => boolean
This is what I'd like to see for example:
const myBasic: Example = {moo: false}
// -> ✅ Valid! Using known keys
const myValid: Example = {hello: 'world', customYo: () => true}
// -> ✅ Valid! "customYo" is a function returning a bool. Good job!
const myInvalid: Example = {hello: 'world', customYo: 'yo!'}
// -> ☠️ Invalid! "customYo" must be a function returning a boolean
Trying to add an index signature to an interface with known keys (i.e. hello?: string, moo?: boolean
) requires all keys to be subsets of the index signature type (in this case, a function returning a boolean
). This obviously fails.
Index signature syntax The syntax of an index signature is pretty simple and looks similar to the syntax of a property, but with one difference. Instead of the property name, you simply write the type of the key inside the square brackets: { [key: KeyType]: ValueType } .
Index signature can be used to define the type of the object whose values are of consistent types or you don't know the structure of the object you are dealing with.
In typescript, Index Signature identifies key type for indexing of an object. Everytime an object in typescript is created and indexing is expected on that object then developers must specify Index Signature .
In TypeScript, an interface is an abstract type that tells the compiler which property names a given object can have. TypeScript creates implicit interfaces when you define an object with properties. It starts by looking at the object's property name and data type using TypeScript's type inference abilities.
The question accepted by the owner (until now) is incorrect.
You need to make the index signature a union type of all the types that can be contained in the interface:
interface IExample {
hello?: string;
moo?: boolean;
[custom: string]: string | boolean | YourFunctionType;
}
interface YourFunctionType {
(v?: any): boolean;
}
Please note that I've extracted your function type into a separate interface to improve readability.
This means, that the explicitly defined properties are well supported by TS:
const test: IExample = <IExample>{};
test.hello.slice(2); // using a string method on a string --> OK
const isHello = test.hello === true; // ERROR (as expected): === cannot be applied to types string and boolean
const isMoo2 = test.moo === true; // OK
However all properties from the index signature now need to be checked using type guards which adds a little bit of a runtime overhead:
test.callSomething(); // ERROR: type 'string | boolean | YourFunctionType' has no compatible call signatures
if (typeof test.callSomething === 'function') { // alternatively you can use a user defined type guard, like Lodash's _.isFunction() which looks a little bit nicer
test.callSomething(); // OK
}
On the other hand: the runtime overhead is necessary because it might be that test
is accessed like this:
const propertyName: string = 'moo';
test[propertyName](); // ERROR: resolves to a boolean at runtime, not a function ...
// ... so to be sure that an arbitrary propertyName can really be called we need to check:
const propertyName2: string = 'arbitraryPropertyName';
const maybeFunction = test[propertyName2];
if (typeof maybeFunction === 'function') {
maybeFunction(); // OK
}
This is not possible, by design https://basarat.gitbooks.io/typescript/docs/types/index-signatures.html
As soon as you have a string index signature, all explicit members must also conform to that index signature. This is to provide safety so that any string access gives the same result.
The only way to get around it is to exploit that each interface can have 2 separate index signatures, one for string
and number
In you example hello
and moo
make the string index unusable, but you can hijack the number index for the custom methods
interface IExample {
hello?: string
moo?: boolean
[custom: number]: (v?: any) => boolean
}
const myBasic: IExample = {moo: false}
// -> ✅ Valid! Using known keys
const myValid: IExample = {hello: 'world', 2: () => true}
// -> ✅ Valid! "customYo" is a function returning a bool. Good job!
const myInvalid: IExample = {hello: 'world', 2: 'yo!'}
// -> ☠️ Invalid! "customYo" must be a function returning a boolean
This works but is hardly an acceptable interface as would lead to unintuitive functions and you would have to call them by array notation
myValid.7() // Cannot invoke an expression whose type lacks a call signature. Type 'Number' has no compatible call signatures.
myValid[2]() // works (but ewwwww what is this!!!)
// could alias to more readable locals later but still ewwwwww!!!
const myCustomFunc = myValid[2]
myCustomFunc() // true
This also has the caveat that the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, javascript will convert the number to a string before indexing into an object
In this case you have no explicit string indexer, so the string index type is the default any
which the numeric indexer type can conform to
IMPORTANT This is just for the science, I don't recommend this as a real life approach!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With