TypeScript: Kräv alla nycklar av en typ som en array


Jag skulle vilja generera en array av strängar som alltid ska innehålla alla nycklar av en given typ.

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?

Jag har försökt type EnforceKeys<T>=Array<keyof T> och medan den typen ger mig autokomplettering och rapporterar olagliga nycklar, så tvingar den inte alla nycklar. Men det är det jag vill.

Och här är bakgrunden till varför jag vill ha det.

// 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];
  });
  // ...
}

Svar #1

Jag stänger detta som en dubblett av den här frågan, men jag kommer att översätta koden för den här frågan nedan, så att du kan se den tillämpad. Vänligen läs det andra svaret för varningarna och förslagen här. Lycka till!

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];
    });
    // ...
}

Länk till kod

Svar #2

Efter kommentaren från @jcalz har jag utvecklat denna kodbit som faktiskt gör vad den ska göra, samtidigt som den är lite ordig.

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"]

Grundidén är att vi kan upprätthålla ett konfigurationsobjekt för en viss typ som kan omvandlas till en lista med nycklar. Det är ännu bättre för mitt användningsfall, eftersom det tillåter utvecklaren att välja in och ut för specifika fält, samtidigt som det säkerställs att varje nytt fält blir konfigurerat.

Och här är den mer återanvändbara koden:

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)
}

Ser tillräckligt bra ut för mig.

Relaterade artiklar