I'm attempting to write a function that returns a value using generics and conditional types like so:
interface Foo<T> {
bar: T extends true ? string : undefined;
id: string;
}
interface Options<T> {
withBar?: T;
}
function createFoo<T extends boolean>({ withBar }: Options<T>): Foo<T> {
return {
id: 'foo',
...(withBar && { bar: 'baz' }),
};
}
The above throws the following type error:
Type '{ bar?: "baz" | undefined; id: string; }' is not assignable to type 'Foo<T>'.
Types of property 'bar' are incompatible.
Type '"baz" | undefined' is not assignable to type 'T extends true ? string : undefined'.
Type 'undefined' is not assignable to type 'T extends true ? string : undefined'.
Can someone help me understand why this is the case? I'm specifying that the type can be undefined, so it should be allow to be undefined.
Additionally I'd like to get the return type of the function given certain params without actually calling it. Is that possible?
e.g. ReturnType<typeof createFoo>
won't give me the correct type for usage createFoo({ withBar: true })
because it doesn't know about the usage yet.
CodePudding user response:
There's an important distinction between a required property than can hold a value or undefined
(the key is always present), and an optional property (the key might not be in the object at all). See exactOptionalPropertyTypes
In your post, you typed bar
on Foo
as always being present, with a conditional value of string
or undefined
, but you were returning an object which only conditionally had the bar
key at all.
In the modification below, I retyped Foo
to reflect an optional property type for bar
based on the same boolean condition.
I also replaced the spread operator syntax with Object.assign
(satisfied the compiler), however, I'm not sure why this makes a difference.
Also, noteworthy: TS complains if you try to spread a non-object type into an object (e.g. undefined
in your post):
const obj = {...undefined};
/* ^^^^^^^^^^^^
Error: Spread types may only be created from object types.(2698) */
type StringId = { id: string };
type Foo<T> = T extends true ? StringId & { bar: string } : StringId;
interface Options<T> {
withBar?: T;
}
function createFoo<T>({ withBar }: Options<T>): Foo<T> {
return Object.assign({id: 'foo'}, (withBar && { bar: 'baz' }));
}
console.log(createFoo({withBar: true})); // { id: "foo", bar: "baz" }
console.log(createFoo({withBar: false})); // { id: "foo" }
console.log(createFoo({})); // { id: "foo" }
Additionally I'd like to get the return type of the function given certain params without actually calling it. Is that possible?
Yes, by adding a conditional return type to the function, which depends on the parameter:
function createFoo<T extends boolean>({ withBar }: Options<T>): T extends true ? Foo<true> : Foo<false> {
return Object.assign({id: 'foo'}, (withBar && { bar: 'baz' }));
}
const foo1 = createFoo({withBar: true}); // StringId & { bar: string }
const foo2 = createFoo({withBar: false}); // StringId
CodePudding user response:
This is a common problem, the generic types do not resolve inside the function implementation. You could write a function overload to make your implementation work:
interface Foo<T> {
bar: T extends true ? string : undefined;
id: string;
}
interface Options<T> {
withBar?: T;
}
function createFoo<T extends boolean>({ withBar }: Options<T>): Foo<T>
function createFoo({ withBar }: Options<boolean>): any {
// The implementation works because the return type is allowed to be anything
return {
id: 'foo',
...(withBar && { bar: 'baz' }),
};
}
// The first function overload is taken because it's the first one to match
// the call parameters
const foo = createFoo({ withBar: false }); // foo is of type Foo<false>