Reputation: 4997
I'm working on converting my little library from JavaScript to TypeScript, and I have a function there
function create(declarations: Declarations) {
Now the declaration is an object the keys of which can be of 2 types:
onMemeber
/ onCollection
, then the value should be a numberIs that possible to enforce with TypeScript? How should I define my Declarations
interface?
Upvotes: 2
Views: 2767
Reputation: 328057
There is no concrete type in TypeScript that represents your Declarations
shape.
I'd call the general idea a "default property" type. (GitHub issue asking for this is microsoft/TypeScript#17867) You want specific properties to be of one type, and then any others to "default" to some other incompatible type. It's like an index signature without the constraint that all properties must be assignable to it.
( Just to be clear, an index signature cannot be used:
type BadDeclarations = {
onMember: number, // error! number not assignable to string
onCollection: number, // error! number not assignable to string
[k: string]: string
};
The index signature [k: string]: string
means every property must be assignable to string
, even onMember
and onCollection
. To make an index signature that actually works, you'd need to widen the property type from string
to string | number
, which probably doesn't work for you. )
There were some pull requests that would have made this possible, but it doesn't look like they are going to be part of the language any time soon.
Often in TypeScript if there's no concrete type that works you can use a generic type which is constrained in some way. Here's how I'd make Declarations
generic:
type Declarations<T> = {
[K in keyof T]: K extends 'onMember' | 'onCollection' ? number : string
};
And here's the signature for create()
L
function create<T extends Declarations<T>>(declarations: T) {
}
You can see that the declarations
parameter is of type T
, which is constrained to Declarations<T>
. This self-referential constraint ensures that for every property K
of declarations
, it will be of type K extends 'onMember' | 'onCollection' ? number : string
, a conditional type that is a fairly straightforward translation of your desired shape.
Let's see if it works:
create({
onCollection: 1,
onMember: 2,
randomOtherThing: "hey"
}); // okay
create({
onCollection: "oops", // error, string is not assignable to number
onMember: 2,
otherKey: "hey",
somethingBad: 123, // error! number is not assignable to string
})
That looks reasonable to me.
Of course, using a generic type isn't without some annoyances; suddenly every value or function that you wanted to use Declarations
with will need to be generic now. So you can't do const foo: Declarations = {...}
. You'd need const foo: Declarations<{onCollection: number, foo: string}> = {onCollection: 1, foo: ""}
instead. That's obnoxious enough that you'd likely want to use a helper function like to allow such types to be inferred for you instead of manually annotated:
// helper function
const asDeclarations = <T extends Declarations<T>>(d: T): Declarations<T> => d;
const foo = asDeclarations({ onCollection: 1, foo: "a" });
/* const foo: Declarations<{
onCollection: number;
foo: string;
}>*/
Okay, hope that helps; good luck!
Upvotes: 2
Reputation:
It seems this is not possible to do, in the TypeScript Deep Dive book there's a section on exactly this. Although you can declare a type you can't actually create an object with it in TS:
// No error on this type declaration:
type Declarations = {
[key: string]: string;
} & {
onMember: number;
onCollection: number;
}
// Error does appear here indicating type `number` is not assignable to type `string`.
const declarations: Declarations = {
onMember: 0,
onCollection: 0,
other: 'Is a string'
}
Upvotes: 1