I have an object like this, and I plan to keep this as a template that takes an arbitrary number of fields. I want to design an interface such that i can supply an array of field names and that it can be slotted into this object as suggested below. but im not sure if im doing it correctly, or over complicating it.
let formResponse: FormResponsePayload<["email", "username"]> = {
status: 'ERROR',
fieldsWithError: ['email', 'username'], //empty or filled with the fields corresponding to keys of fields
fields: {
email: {
field_name: 'email',
status: 'error',
message: ''
},
username: {
field_name: 'email',
status: 'error',
message: ''
}
}
}
the answer I've come up with
//i was looking for something that could unpack my generic of string[]. i found Unpacked on another Stack answer. not totally sure how it works.
type Unpacked<T> = T extends (infer U)[] ? U : T;
interface CheckField<A extends string> {
field_name: A,
status: string,
message: string
}
export interface FormResponsePayload<A extends string[]> {
status: 'ERROR' | 'SUCCESS'
fieldsWithError: Unpacked<A>[],
fields: {
[key in Unpacked<A>]: CheckField<Unpacked<A>>
}
}
CodePudding user response:
You are on the right track indeed!
Note that when you specify a generic type, it should still be a type, so in FormResponsePayload<["email", "username"]>
, the generic type part cannot be an actual array, it must be converted to a type.
For example:
const fieldNames = ['email', 'username'] as const; // Assert as const to infer a tuple instead of an array string []
let formResponse: FormResponsePayload<typeof fieldNames>
Then to get the possible tuple values, there is no longer need for an "Unpacked" utility type. TypeScript provides indexed access type on array/tuple:
type FieldNames = typeof fieldNames[number]
// ^? type FieldNames = "email" | "username"
Then your generic interface can be written as:
interface FormResponsePayload<FieldNames extends readonly string[]> {
status: 'ERROR' | 'SUCCESS'
fieldsWithError: FieldNames[number][],
fields: {
[key in FieldNames[number]]: CheckField<key>
}
}
Let's test its usage:
const a: FormResponsePayload<typeof fieldNames> = {
status: 'SUCCESS',
fieldsWithError: ['email'], // TS catches typos
fields: { // TS requires all field names
email: {
field_name: 'email', // TS enforces same value as the field name
status: '',
message: ''
},
username: {
field_name: 'username',
status: '',
message: ''
}
}
}
We can even further simplify the declaration by using a union for the possible field name, as done for many other TypeScript utility types (e.g. Pick<Todo, "title" | "completed">
, Omit<Todo, "completed" | "createdAt">
, etc.):
// A union of string literals actually extends string
interface FormResponsePayload2<FieldNamesUnion extends string> {
status: 'ERROR' | 'SUCCESS'
fieldsWithError: FieldNamesUnion[],
fields: {
[key in FieldNamesUnion]: CheckField<key>
}
}
Its usage is also slightly simplified (no longer need for typeof
):
const b: FormResponsePayload2<'email' | 'username'> = {
status: 'SUCCESS',
fieldsWithError: ['email'], // TS catches typos
fields: { // TS requires all field names
email: {
field_name: 'email', // TS enforces same value as the field name
status: '',
message: ''
},
username: {
field_name: 'username',
status: '',
message: ''
}
}
}