skip to Main Content
interface Item {
  slug: string;
  description: string;
}

interface Params {
  id: number;
  items: Item[];
}

function test(params: Params) {
  const result = {};
  
  for (const item of params.items) {
    result[item.slug] = "Hello";
  }
  
  return result;
}


const data = {
  id: 3,
  items: [
    { slug: "one", description: "This is one" },
    { slug: "two", description: "This is two" },
    { slug: "three", description: "This is three" },
  ]
}

const final = test(data)

OK. I have data and pass it to the test function. The test function returns an object with keys as the value of slugs in the items and a string value. So I want the type of final variable to be like this:

{
  one: string;
  two: string;
  three: string;
}

and not this:

Record<string, string>

I mean it must extract slug values from items and create an object with it. explicitly tell the type. How can I define the return type of test function to achieve this?
thanks.

2

Answers


  1. If your data is "static" you can mark it as constant with as const now your type for slug is not string but string literals:

    
    interface Item {
      slug: string;
      description: string;
    }
    
    interface Params {
      id: number;
      items: Item[];
    }
    
    const data = {
      id: 3,
      items: [
        { slug: "one", description: "This is one" },
        { slug: "two", description: "This is two" },
        { slug: "three", description: "This is three" },
      ]
    } as const
    
    type Data = typeof data
    
    type Slug = Data["items"][number]["slug"]
    
    type Mapping = Record<Slug,string>
    

    Or here a playground link

    Login or Signup to reply.
  2. First, you’ll need to change the way you initialize data. Without any context, TypeScript looks at the object literal with a property like {slug: "one"} and infers the type as {slug: string}. It doesn’t know that you will later want to know the literal type "one". That means you’ve lost the information you care about before you even call test(data).

    The easiest approach is to use a const assertion, which infers literal types for literal values, and readonly tuples for array literals:

    const data = {
      id: 3,
      items: [
        { slug: "one", description: "This is one" },
        { slug: "two", description: "This is two" },
        { slug: "three", description: "This is three" },
      ]
    } as const; 
    

    Now we can focus on test() and the types it operates on. If you want to keep track of the actual literal types of the slug properties passed into test(), so that the output type of test depends on its input type, you’ll need to change test()‘s call signature to be generic. That implies that Params and Item also need to be generic. Possibly like this:

    interface Item<K extends string = string> { 
      slug: K;
      description: string;
    }
    
    interface Params<K extends string = string> { 
      id: number;
      items: readonly Item<K>[];
    }
    

    These types are generic in K, the type of the slug property. I’ve given K a [default type argument](Note that https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) of just string, so that you can keep referring to the Item and Params types without a type argument and it will be the same as before.

    Oh, and I made items a readonly array because const assertions produce readonly arrays, and I didn’t want to have to make everything more complicated by trying to work around that. Generally speaking if you want to accept an array and you don’t intend to mutate it, you can add readonly to the type and it will work just as well. All arrays in TypeScript are assignable to their readonly versions but not vice versa (that is, every string[] is considered a readonly string[], but not every readonly string[] is considered a string[]). The name readonly is actually confusing that way; really, readonly string[] means "a readable array of strings, which may or may not be writable" and string[] means "a readable and writable array of strings".


    So, finally, we can make test generic and accept a Params<K>:

    function test<K extends string = string>(params: Params<K>) {
      const result = {} as {[P in K]: string} // <-- need assertion
    
      for (const item of params.items) {
        result[item.slug] = "Hello";
      }
    
      return result;
    }
    

    And I’ve asserted that result is of type {[P in K]: string}, a mapped type equivalent to Record<K, string> using the Record utility type. It means "an object type whose keys are of type K and whose properties are of type string". It needs an assertion because {} is clearly not of that type (it has no keys at all), so we have to lie to the compiler. But the assumption is that your for loop makes changes the assertion from a lie to the truth before the function returns. And now the return type of test depends on K in the way you want:

    const final = test(data)
    /* const final: {
        one: string;
        two: string;
        three: string;
    } */    
    

    Here you can see that test(data) infers K as "one" | "two" | "three", and so the return type is {one: string; two: string; three: string} as desired.

    Playground link to code

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search