13

I'd like to generate an array of strings that should always contain all keys of a given type.

interface User {
  id: number
  name: string
}

// should report an error because name is missing
const allUserFields: EnforceKeys<User> = ["id"];

type EnforceKeys<T> = any; // what to use here?

I've tried type EnforceKeys<T> = Array<keyof T> and while that type gives me auto-completion and reports illegal keys, it doesn't enforce all keys. However, that's what I want.

And here is the background why I want to have it.

// this type contains updatable fields and can be given to api/client interface
type UserUpdate = Pick<User, "name">

// this should always contain all keys to keep it in sync 
const updateableFields: EnforceKeys<UserUpdate> = ['name']

// simple example for using the array to just update updatable fields
function updateUser(user: User, update: UserUpdate) {
  updateableFields.forEach(field => {
    user[field] = update[field];
  });
  // ...
}
K. D.
  • 4,041
  • 9
  • 48
  • 72
  • I'm not sure such a type *can* exist as purely a type. Unless you are only dealing with hard-coded arrays, then you can treat them as tuples. However, I suspect you don't hence you wouldn't have an array to begin with. If the arrays are in any way dynamic, then you can't have a compile-time check. – VLAZ Jan 16 '20 at 16:59
  • Thank you. Well, I'm dealing with hard-coded types, so I guess it should be possible in theorie. I've added the example using `Array` which is close to what I'm looking for, except it doesn't require all keys. – K. D. Jan 16 '20 at 17:03
  • I don't think it's possible at all. Why do you need an array? – Roman Koliada Jan 16 '20 at 17:04
  • @RomanKoliada I have a type that defines a subset of fields via `Pick` and it is used to determine updatable fields of the base type. Now I want to translate this info into code, where the actual update happens. Do you understand what I'm talking about? – K. D. Jan 16 '20 at 17:07
  • @K.D. Check this out: https://stackoverflow.com/a/53395649/5126411 – Roman Koliada Jan 16 '20 at 17:07
  • @K.D. OK, it should be possible in theory indeed. I just don't know how. You can get the keys of an object as a type but that just means that you can have `["id", "id", "id"]`, for example, doesn't ensure uniqueness or completeness. So, I suppose the keys could be convertable to a tuple that enumerates all keys. Then only an array of all keys would match it. Also, the array will need to follow the order of the keys. – VLAZ Jan 16 '20 at 17:08

2 Answers2

2

After the comment of @jcalz I've developed this piece of code that actually does what it should do, while being a little bit verbose.

type BooleanMap<T> = { [key in keyof T]: boolean }

const updatableFieldsConfig: BooleanMap<UserUpdate> = {
  name: true,
}

const updatableFields = Object.entries(updatableFieldsConfig)
  .filter(([key, value]) => !!value)
  .map(([key, value]) => key)
// ["name"]

The basic idea is that we can enforce a configuration object for a given type that can be transformed into an array of keys. That is even better for my use case, since it allows the developer to opt in and out for specific fields, while enforcing that every new field gets configured.

And here is the more reusable code:

interface UserUpdate {
  name: string
}

const updatableFields = getWhitelistedKeys<UserUpdate>({
  name: true,
})

function getWhitelistedKeys<T>(config: { [key in keyof T]: boolean }) {
  return Object.entries(config)
    .filter(([_, value]) => !!value)
    .map(([key]) => key as keyof T)
}

Looks good enough for me.

K. D.
  • 4,041
  • 9
  • 48
  • 72
1

I'm closing this as a duplicate of this question, but I'll translate the code for this question below, so you can see it applied. Please read the other answer for the caveats and suggestions here. Good luck!

interface User {
    id: number
    name: string
}

type Cons<H, T extends readonly any[]> = H extends any ? T extends any ?
    ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
// illegally recursive, use at your own risk
type UnionToAllPossibleTuples<T, U = T, N extends number = 15> = T extends any ?
    Cons<T, Exclude<U, T> extends infer X ? {
        0: [], 1: UnionToAllPossibleTuples<X, X, Prev[N]>
    }[[X] extends [never] ? 0 : 1] : never> :
    never;

type AllPossibleTuplesOfUserKeys = UnionToAllPossibleTuples<keyof User>;
const allUserFields: AllPossibleTuplesOfUserKeys = ["id", "name"]; // okay
const missing: AllPossibleTuplesOfUserKeys = ["id"]; // error
const redundant: AllPossibleTuplesOfUserKeys = ["id", "id", "name"]; // error
const extra: AllPossibleTuplesOfUserKeys = ["id", "name", "oops"]; // error


type NoRepeats<T extends readonly any[]> = { [M in keyof T]: { [N in keyof T]:
    N extends M ? never : T[M] extends T[N] ? unknown : never
}[number] extends never ? T[M] : never }

const verifyArray = <T>() => <U extends NoRepeats<U> & readonly T[]>(
    u: (U | [never]) & ([T] extends [U[number]] ? unknown : never)
) => u;

const verifyUserKeyArray = verifyArray<keyof User>()
const allUserFieldsGeneric = verifyUserKeyArray(["id", "name"]); // okay
const missingGeneric = verifyUserKeyArray(["id"]); // error
const redundantGeneric = verifyUserKeyArray(["id", "id", "name"]); // error
const extraGeneric = verifyUserKeyArray(["id", "name", "oops"]); // error


// this type contains updatable fields and can be given to api/client interface
type UserUpdate = Pick<User, "name">

// this should always contain all keys to keep it in sync 
const updateableFields: UnionToAllPossibleTuples<keyof UserUpdate> = ['name']

// simple example for using the array to just update updatable fields
function updateUser(user: User, update: UserUpdate) {
    updateableFields.forEach(field => {
        user[field] = update[field];
    });
    // ...
}

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 3
    Thank you, but I guess my solution is simpler, isn't it? Feel free to keep it closed anyway and thank you for helping out. – K. D. Jan 16 '20 at 17:33
  • 4
    It's not *quite* a duplicate; this question requires that the array contain all of the elements of a particular type, whereas the other question requires that but also requires the array not to have any duplicates. I have a solution which works for the former but not the latter, but it's quite a bit simpler than the accepted answer to the latter, and doesn't require the compiler to compute all possible permutations of a tuple. – kaya3 Jan 16 '20 at 18:03
  • 1
    Also, I think my actual use case is very common and can be solved easily with my supplied answer (that I will accept soon). I'm not sure if that's true for the other question. – K. D. Jan 16 '20 at 19:35
  • 1
    The asker of the likned question also ends up using a mapped object instead of an array, for the same reason, as mentioned in the linked answer: "Honestly I'd suggest trying to refactor your code not to require TypeScript to enforce this. The easiest thing is to require an object have these values as keys (e.g., just make a value of type `T` or possibly `Record`) and use that instead of (or before producing) an array." – jcalz Jan 16 '20 at 19:44