myId
, add a default property such as { myId: string }
.myId
, then override it with the passed-in property type.Examples:
/**
* When the input object does not contain the key `myId`,
* a default property { myId: string } is added.
*/
type peopleDefaultId = People<{ name: string }>
/* expected:
{
myId: string // default
name: string
}
*/
/**
* When the input object contains `myId`,
* it will override with the input property type.
*/
type peopleNumberId = People<{ myId: number, name: string }>
/* expected:
{
myId: number // override
name: string
}
*/
/**
* Represents a generic type of People. When the input object contains the `myId` key,
* the type of the `myId` property will be overridden with the input type. Otherwise,
* a default property `{ myId: string }` is added. The `myId` key type retains its
* original optional/non-optional status.
*/
type peopleOptionalStringId = People<{ myId?: string, name: string }>
/* expected:
{
myId?: string // override
name: string
}
*/
type ExtendsDefault<T, Default> = T & Partial<Pick<Default, Exclude<keyof Default, keyof T>>>;
type People<T> = ExtendsDefault<T, {
myId: string
}>
|
Operator// Incorrect example
type People<T> = {
myId: string
} | T
If the input object already includes the myId
key (e.g., myId: number
),
the resulting myId
type would erroneously become string | number
.
type peopleNumberId = People<{ myId: number, name: string }>
// Expected:
// {
// myId: number,
// name: string
// }
// Actual output:
// {
// myId: string | number, // Error!
// name: string
// }
extends
to Determine Type Usageinterface People<T> {
myId: T extends { myId: infer U } ? U : string
}
First, check whether the input generic object has myId
. If it does, extract the corresponding type; otherwise, default to string.
Although this seems to solve the first pitfall, on closer inspection, since myId
above is not an optional key, it forces inclusion of the myId
key even when the input generic type is an optional property like myId?: string
, which is inconvenient.
// [error] Property 'myId' is missing in type '{}' but required in type 'People<{ myId?: string }>'.
const people: People<{ myId?: string }> = {}
^^ // error
Conversely, if the default myId
is optional, defining a non-null property would erroneously force the property to become optional.
interface People<T> {
myId?: T extends { myId: infer U } ? U : string
}
const people: People<{ myId: string }> = {}
// output: { myId?: string } <- error: expected to be { myId: string }
type People<T extends { myId?: any }> = 'myId' extends keyof T
? T extends { myId: infer U } ? { myId: U }
: T extends { myId?: infer U } ? { myId?: U } : never
: { myId: string };
This approach meets all requirements, but the code appears somewhat lengthy and complex. A later improvement used [myId in keyof T]
to directly determine if myId
is an optional property and used it as the key, leading to the following version:
type People<T extends { myId?: any }> = 'myId' extends keyof T
? { [myId in keyof T]: T[myId] }
: { myId: string };
Additionally, a version was created to allow input of a custom key
, which is more convenient if the property is not myId
.
type DefaultProperty<T extends { [K in keyof T]?: any }, K extends keyof any> = K extends keyof T
? { [K in keyof T]: T[K] }
: { [K in keyof any]: number };
// usage
type People<T, K> = DefaultProperty<T, 'myId'>
However, it was realized that this approach is inconvenient for defining different default types or multiple default properties at once.
Returning to the drawing board, the idea of passing the default object as a generic during the definition of the generic type was considered, conceptually like this:
type ExtendsDefault<T, Default> = ... ;
type People<T> = ExtendsDefault<T, {
myId: string
}>
This would greatly enhance reusability and support for multiple default properties.
After extensive review of TypeScript documentation, it was discovered that using Pick
combined with Exclude
could "replace" the properties of one object with another:
Pick
selects the necessary keys from Default
.Exclude
excludes keys already present in T
from Default
.Partial
makes the remaining keys in Default
optional.type ExtendsDefault<T, Default> = T & Partial<Pick<Default, Exclude<keyof Default, keyof T>>>;
type People<T> = ExtendsDefault<T, {
myId: string
}>