Home > database >  Incorrect return type for conditional generics
Incorrect return type for conditional generics

Time:12-17

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.

Live example here

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) */

TS Playground

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:

TS Playground

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>

TypeScript playground

  • Related