Reputation: 1964
I have a base interface and another interface extends it. I'm trying to write a function, that will get some arguments from the base interface and some will be prefilled. If there are any new properties that do not exist in the base interface, it is also accepted as arguments since they can't be pre-filled.
interface Foo {
prop: number;
hidden: number;
}
interface Bar extends Foo {
prop: 1;
hidden: 2;
newProp: string;
}
const createThing = <T extends Foo>({
prop,
...rest
}: Omit<T, keyof Foo> & Pick<T, 'prop'>): T => {
return {
prop,
hidden: 3, // <-- I want this prefilled, but it must be 2, not 3.
...rest,
}; /* if I write `as T`, it works, but it allows me to pass 3 to the
`hidden` property, which will be wrong. */
/* but this whole return complains, that T could be instantiated with
a different subtype of constraint 'Foo'. */
};
const thing = createThing<Bar>({ prop: 1, newProp: 'foo' });
thing.hidden === 2; /* this will be false, because I returned 3
from the createThing function. */
What is the proper way to achieve this?
Upvotes: 0
Views: 149
Reputation: 42188
As I understand this, your createThing<T>
method can accept any properties of T (Partial<T>
) and it requires the inclusion of any new properties were not part of Foo (Omit<T, keyof Foo>
). That is,
type Props<T extends Foo> = Partial<T> & Omit<T, keyof Foo>;
Where things get trickier is when T has the same property name as in Foo, but with a more specific type. ie. hidden
must be 2
, not just any number
. You could require that these properties be included in Props, but you've said that you want them pre-filled.
I've written this where you have a separate set of defaults for each thing interface, and you pass those in to createThing. Otherwise how would it know what is and isn't an appropriate default?
If I understand this correctly, the extra properties don't need defaults, but any of the Foo interface properties need a default if they are made more specific. Here I am using a mapped type to figure out which properties you need.
type NarrowedKeys<T extends Foo> = {
// there's probably a cleaner way to write this
[K in keyof T]: K extends keyof Foo ? Foo[K] extends T[K] ? never : K : never
}[keyof T];
type NarrowedDefaults<T extends Foo> = Pick<T, NarrowedKeys<T>>
At this point, I still get the 'T' could be instantiated with a different subtype of constraint 'Foo'
message, but I'm content to hush it with as T
because I cannot pass the invalid value of hidden in either NarrowedDefaults<Bar>
or Props<Bar>
.
const fooDefaults: Foo = {
prop: 1,
hidden: 5,
}
const createThing = <T extends Foo>(tDefaults: NarrowedDefaults<T>) => (props: Props<T>): T => {
return {
...fooDefaults,
...tDefaults,
...props,
} as T;
};
const createBar = createThing<Bar>({
prop: 1,
hidden: 2,
});
const thing = createBar({prop: 1, newProp: 'foo'});
Upvotes: 1