简体   繁体   中英

Typescript: Typing Function Return Type based on Parameters

I was wondering if there is way to type the return of a function based on the input given to it. Here is an example of what I am thinking:

function toEnum(...strings: string[]) {
  const enumObject = {};

  strings.forEach((str) => {
    enumObject[str.toUpperCase()] = str;
  });

  return enumObject;
}

const myEnum = toEnum('one', 'two', 'three')

Is there a way to type this function so that we know that myEnum looks like:

{
  ONE: 'one',
  TWO: 'two',
  THREE: 'three'
}

edit:

as @dariosicily mentioned, we could type enumObject using a Record<string, string> or index signatures, but I am wondering if there is a way to know the actual keys present in the return object based on the params passed in.

There is an intrinsic Uppercase<T> string manipulation utility type which, when given a string literal type as input, produces an uppercase version as output. So Uppercase<"abc"> and "ABC" are the same type. Using this type we can create a mapped type with remapped keys to express the output type of toEnum() , given the union of the string literal types of its arguments:

function toEnum<K extends string>(...strings: K[]): { [P in K as Uppercase<P>]: P } {
    const enumObject: any = {};

    strings.forEach((str) => {
        enumObject[str.toUpperCase()] = str;
    });

    return enumObject;
}

Note that toEnum() is generic in K , the union of the element types of the strings array. Note that K is constrained to string so that strings is indeed an array of strings, and because this constraint gives the compiler a hint that we want to infer string literal types for its elements instead of just string . You definitely need to use generic here, otherwise you'd just get Record<string, string> out of the function.

The type {[P in K as Uppercase<P>]: P} iterates over every string P in the original K union and remaps it to an uppercase version as the key, and then uses just the same type P as the value. That's the type you wanted.

Also note that I gave enumObject the any type so as to opt out of strict type checking inside the implementation of toEnum() ; the compiler is unable to follow the logic that enumObject[str.toUpperCase()]=str will be an appropriate operation on a value of type {[P in K as Uppercase<P>]: P} , so we won't even make it try.

Anyway you can test that it does what you want:

const myEnum = toEnum('one', 'two', 'three', "fortyFive");
/* const myEnum: {
    ONE: "one";
    TWO: "two";
    THREE: "three";
    FORTYFIVE: "fortyFive";
} */

console.log(myEnum.THREE) // "three" both at compile and runtime

In the comments you mentioned that for something like fortyFive , you'd like the key to be FORTY_FIVE instead of FORTYFIVE . That is, you don't just want the key to be an uppercase version of the input. You want the input to be interpreted as lower camel case and the output to be all-upper snake case (also known as SCREAMING_SNAKE_CASE).

This is also possible in TypeScript, using template literal types to split a string literal type into characters, and recursive conditional types to operate on these characters programmatically.

First let's do it at the type level:

type LowerPascalToUpperSnake<T extends string, A extends string = ""> =
    T extends `${infer F}${infer R}` ? LowerPascalToUpperSnake<R,
        `${A}${F extends Lowercase<F> ? "" : "_"}${Uppercase<F>}`
    > : A;

Note that it is useful to have a function that does the same thing at the value level:

function lowerPascalToUpperSnake<T extends string>(str: T) {
    return str.split("").map(
        c => (c === c.toLowerCase() ? "" : "_") + c.toUpperCase()
    ).join("") as LowerPascalToUpperSnake<T>
}

Both the type and the function behave similarly; the idea is to iterate over each character of the string, insert an underscore if and only if the current character is not lowercase, and then insert an uppercase version of the current character. You can verify that this works:

const test = lowerPascalToUpperSnake("abcDefGhiJklmNop");
// const test: "ABC_DEF_GHI_JKLM_NOP"
console.log(test); // "ABC_DEF_GHI_JKLM_NOP" 

The value at runtime and the type computed by the compiler agree.

And now we can use the "lower-Pascal-to-upper-snake" operation in toEnum() instead of the original uppercase operation:

function toEnum<K extends string>(...strings: K[]): 
  { [P in K as LowerPascalToUpperSnake<P>]: P } {
    const enumObject: any = {};

    strings.forEach((str) => {
        enumObject[lowerPascalToUpperSnake(str)] = str;
    });

    return enumObject;
}

And see it in action:

const myEnum = toEnum('one', 'two', 'three', "fortyFive");
/* const myEnum: {
    ONE: "one";
    TWO: "two";
    THREE: "three";
    FORTY_FIVE: "fortyFive";
} */

console.log(myEnum.FORTY_FIVE) // "fortyFive"

Looks good!

Playground link to code

The problem is due to the fact that while in the javascript language the line enumObject[str.toUpperCase()] = str; assignment is legitimate with typescript the same line causes an error because you have not explicitely declared the index signature of the enumObject or explicitely declared it as any .

In this cases one way to solve the issue is use the builtinRecord utility type applying it to your enumObject like below:

function toEnum(...strings: string[]) {
    const enumObject: Record<string, string> = {};
    strings.forEach((str) => {
        enumObject[str.toUpperCase()] = str;
    });
    return enumObject;
}

const myEnum = toEnum('one', 'two', 'three')
//it will print { ONE: 'one', TWO: 'two', THREE: 'three' }
console.log(myEnum);

Edit : answering to the question

there is a way to know the actual keys present in the return object based on the params passed in

You can use the Object.keys method and return the keys as an Array<string> :

const myEnum = toEnum('one', 'two', 'three')
//it will print [ONE, TWO, THREE]
const keys = (Object.keys(myEnum) as Array<string>);

If you want to create a new type from keys you can use typeof :

const keys = (Object.keys(myEnum) as Array<string>);
//type KeysType = 'ONE' | 'TWO' | 'THREE'
type KeysType = typeof keys[number];

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM