Josejulio
Josejulio

Reputation: 1295

Is possible to tell that a function to narrow down the types and not expand?

I'm trying to write a tool to generate API clients with typesafe calls, adding libs that will take care of validating the input and sorting it out.

I would like to implement a global transforming option to allow users to modify the responses based on the type given.

Suppose we have a set of types that all share a Base i.e.

type Base<NAME extends string, T> = {
  name: NAME;
  value: T;
}

// All the possible responses given to the API
type ALL = Base<'Animal', Animal> | Base<'ZooKeeper', ZooKeeper> | Base<'Visitor', Visitor>;

And I want to write a function to transform all the Animal to TaggedAnimal and ZooKeeper to Keeper i.e.

const transformer = (value: ALL) => {
  if (value.name === 'Animal') {
     return {
         name: 'TaggedAnimal',
         value: {
            ...value.value,
            tag: 'mytag' // or something else made up of animal attributes
         }
     } as Base<'TaggedAnimal', TaggedAnimal>;
  } else if (value.name === 'ZooKeeper') {
    return {
      name: 'Keeper',
      value: {
        id: value.value.id
      }
    } as Base<'Keeper', Keeper>;
  }

  return value;
}

So far so good, but the problem lies when I try to use this function on a specic API.

const getAnimal = (): Base<'Animal', Animal> => {
  // do network request, validation, etc
  return {
      name: 'Animal',
      value: {
        id: '123',
        name: 'Lion'
    }
  } as Base<'Animal', Animal>;
}

const animal = getAnimal(); // Good! type of animal: Base<'Animal', Animal>
const tanimal = transformer(animal); // :/! type of tanimal: Base<'TaggedAnimal', TaggedAnimal> | Base<'Keeper', Keeper> | Base<'Visitor', Visitor>;

I understand that this happens because the transformer expects all the types and returns a fixed subset (given by the function).

Is there any way to go about this with the current version of typescript (4.7)?

I have tried using generics to narrow down i.e.:

const transformer = <IN extends ALL>(value: IN) => {
    // ...
}

const tanimal = transformer(animal); // :/! type of tanimal: Base<'Animal', Animal> | Base<'TaggedAnimal', TaggedAnimal> | Base<'Keeper', Keeper>;

Playground link

Upvotes: 1

Views: 35

Answers (1)

You need to overload your function:

interface Animal {
  id: string;
  name: string;
}

interface ZooKeeper {
  id: string;
  shift: string;
}

interface Visitor {
  id: string;
  fee: number;
}

interface TaggedAnimal extends Animal {
  tag: string;
}

interface Keeper {
  id: string;
}

type Base<NAME extends string, T> = {
  name: NAME;
  value: T;
}

// All the possible responses given to the API
type ALL = Base<'Animal', Animal> | Base<'ZooKeeper', ZooKeeper> | Base<'Visitor', Visitor>;

function transformer(value: Base<'Visitor', Visitor>): Base<'Visitor', Visitor>
function transformer(value: Base<'ZooKeeper', ZooKeeper>): Base<'Keeper', Keeper>;
function transformer(value: Base<'Animal', Animal>): Base<'TaggedAnimal', TaggedAnimal>
function transformer(value: ALL) {
  if (value.name === 'Animal') {
    return {
      name: 'TaggedAnimal',
      value: {
        ...value.value,
        tag: 'mytag' // or something else made up of animal attributes
      }
    }
  } else if (value.name === 'ZooKeeper') {
    return {
      name: 'Keeper',
      value: {
        id: value.value.id
      }
    }
  }

  return value;
}

const getAnimal = (): Base<'Animal', Animal> => ({
  name: 'Animal',
  value: {
    id: '123',
    name: 'Lion'
  }
})

const animal = getAnimal(); // Good! type of animal: Base<'Animal', Animal>
const tanimal = transformer(animal); // Base<"TaggedAnimal", TaggedAnimal>

Playground

Upvotes: 2

Related Questions