Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: Require all keys of a type as array [duplicate]

Tags:

typescript

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];
  });
  // ...
}
like image 303
K. D. Avatar asked Jan 16 '20 16:01

K. D.


2 Answers

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.

like image 103
K. D. Avatar answered Sep 23 '22 23:09

K. D.


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

like image 27
jcalz Avatar answered Sep 21 '22 23:09

jcalz