简体   繁体   中英

Typescript: How to make type that accepts object which keys match generic but all values are map functions of value argument

I want to achieve type that says: "I have all keys recursively same as type T but all my values are function that accept such key value and map it.

I've tried the code below but it doesn't infer return types of specified functions and recursiveness is wrong

export type ObjectButAllKeyIsValueFn<T> = {
    [K in keyof T]?: ObjectButAllKeyIsValueFn<T[K]> | (<R>(value: T[K]) => R)
}


interface User {
    name: string,
    surname: string,
    address: {
        city: string,
        country: string,
        street: string
    }
}

const z: ObjectButAllKeyIsValueFn<User> = {
    name: value => `Hello ${value}`,
    address: {
        city: city => 1
    }
};

Update: I want to achieve something like this but recursive, so I have to properly map StupidPattern to Pattern

type StupidPattern<T> = {
    [K in keyof T]: StupidPattern<T[K]> | ((value: T[K]) => unknown)
}

type Pattern<T> = {
    [K in keyof T]: (value: T[K]) => unknown
}

type Result<T, P = Pattern<T>> = {
    [K in keyof P]: K extends keyof T
        ? P[K] extends (value: T[K]) => infer R
            ? R
            : never
        : never
}

declare function map <T> (pattern: Pattern<T>): (target: T) => Result<T, Pattern<T>>
declare function map <T> (pattern: Pattern<T>, target: T): Result<T, Pattern<T>>;

The type (<R>(value: T[K]) => R) is not what you are looking for; essentially the generic R is quantified the wrong way. It means "a function whose input is of type T[K] and whose output is any type R that the caller specifies. That's not really possible to implement safely. Presumably you really mean that the implementer of the function should be able to specify R , but there's no clean way in TypeScript to represent that with generics... you'd maybe put R as another generic parameter of ObjectButAllKeyIsValueFn , but then you'd need one R per key of T . Let's back up and look at this a different way:

How about we define ObjectButAllKeyIsValueFun as this type:

export type ObjectButAllKeyIsValueFn<T> = {
  [K in keyof T]?: ObjectButAllKeyIsValueFn<T[K]> | ((value: T[K]) => unknown)
}

This type captures the idea that function-valued properties are allowed to return absolutely any type as long as you're concerned. But of course, simply annotating a value as type ObjectButallKeyIsValueFn<User> would throw away a bunch of information about that value's type; you wouldn't know what type each function returns, or even if any particular property is a function at all.

So let's give up on annotating variables as that type. Instead, when we create values, we make sure they are assignable to that type without widening them. This will give us all the IntelliSense you want. Here's a helper function and its application to User :

const asObjType = <T>() => <U extends ObjectButAllKeyIsValueFn<T>>(u: U) => u;

const asObjUser = asObjType<User>();

And here's how you call asObjUser() :

const z = asObjUser({
  name: value => `Hello ${value}`,
  address: {
    city: city => 1
  }
});

/* const z: {
    name: (value: string) => string;
    address: {
        city: (city: string) => number;
    };
} */

console.log(z.name("Fred")); // Hello Fred

That works and keeps track of the specific type of z . It also catches errors like this:

const oops = asObjUser({
  name: 123, // error!
  // number is not assignable to string | ((value: string) => unknown) | undefined
});

So, hopefully that's enough for you to make progress. Good luck!

Playground link to code

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