Max Gordon
Max Gordon

Reputation: 5467

Infer type based on input structure

I'm struggling with inferring type based upon input parameters. Basically I want to use the structure of an object to infer the return type (although not just the . I think the trick is to use a conditional type but I can't get it to work:

type Active = { name: string; active: true };
type Inactive = { name: string; active: false };
type IdWithAct = Active | Inactive;

export const getIdWithType = <
  Arg extends IdWithAct,
  T extends Arg extends Active ? 'ActiveItem' : 'InactiveItem'
>({
  name,
  active,
}: IdWithAct): {name: string, typeName: T } =>  ({
    name,
    typeName: active ? 'ActiveItem' : 'InactiveItem',
  });

See playground. I've looked at the infer solutions but the examples are too trivial for what (at least I think) I'm trying to accomplish...

Edit

Added examples from the comments.

Overloading

As suggested by the comments there is the overloading option, unfortunately TypeScript seems to have issues with calling the function within another:

type ActivityWithName<T> = { name: string, active: T}
type IdWithAct = ActivityWithName<true> | ActivityWithName<false>;

type Types = 'ActiveItem' | 'InactiveItem'
type Ret<T> = { name: string, typeName: Types }

function getFragNameAndIdFromActive(args: ActivityWithName<true>): Ret<'ActiveItem'>
function getFragNameAndIdFromActive(args: ActivityWithName<false>): Ret<'InactiveItem'>
function getFragNameAndIdFromActive<Arg extends IdWithAct>({
  name,
  active,
}: IdWithAct): Ret<Types> {
  return  {
    name,
    typeName: active ? 'ActiveItem' : 'InactiveItem',
  }
}

const retActive = getFragNameAndIdFromActive({ name: 'activeItem', active: true })
const retInactive = getFragNameAndIdFromActive({ name: 'activeItem', active: false })

function test(active: boolean) {
  // active complaints: Type 'boolean' is not assignable to type 'false'.
  const ret = getFragNameAndIdFromActive({ name: 'activeItem', active })
}

See playground. The solution is appealing as it is rather explicit with the intentions of the overloadning.

Runtime assertion

The suggestion with the runtime assertion works but it is in my opinion a little too magic and if the return object is different by more than a single value it could very well become rather messy:

type IdWithAct<T> = { name: string; active: T };
type Status<T> = T extends true ? 'ActiveItem' : 'InactiveItem'

export const getIdWithType = <T extends boolean>({
    name,
    active,
}: IdWithAct<T>) => ({
    name,
    typeName: (active ? 'ActiveItem' : 'InactiveItem') as Status<T>,
});

const foo = getIdWithType({ name: 'x', active: true }) // { name: string; typeName: "ActiveItem"; }
const bar = getIdWithType({ name: 'x', active: false }) // { name: string; typeName: "InactiveItem"; }

const test = (active: boolean) => getIdWithType({ name: 'x', active })

Upvotes: 0

Views: 120

Answers (1)

You need to add one more overload:


type ActivityWithName<T> = { name: string, active: T}
type IdWithAct = ActivityWithName<boolean>

type Types = 'ActiveItem' | 'InactiveItem'
type Ret<T extends Types> = { name: string, typeName: T } // use generic parameter

function getFragNameAndIdFromActive<T extends true>(args: ActivityWithName<T>): Ret<'ActiveItem'>
function getFragNameAndIdFromActive<T extends false>(args: ActivityWithName<T>): Ret<'InactiveItem'>
function getFragNameAndIdFromActive(args: ActivityWithName<boolean>): Ret<Types> // less specific overload
function getFragNameAndIdFromActive({
  name,
  active,
}: IdWithAct): Ret<Types> {
  return  {
    name,
    typeName: active ? 'ActiveItem' : 'InactiveItem',
  }
}

const retActive = getFragNameAndIdFromActive({ name: 'activeItem', active: true })
const retInactive = getFragNameAndIdFromActive({ name: 'activeItem', active: false })

function test(active: boolean) {
  const ret = getFragNameAndIdFromActive({ name: 'activeItem', active }) // ok
}

Playground

Upvotes: 1

Related Questions