I'm trying to type a function witch run a callback for each array item and extend that item with a new properties:
interface Row {
a: number
}
export function extender<
Data extends Row[],
T extends object,
Props extends keyof T
>(data: Data, extend: (row: Row) => T, newProp?: Props): (T & Row)[] {
return data.map(row => ({
...row,
...extend(row)
}))
}
This draft seems to be working when I call
extender([{ a: 1 }, { a: 2 }], () => ({ once: 1, more: 2 }), 'once')
But once I trying to use row
argument in extend
callback, I get an error Argument of type '"once"' is not assignable to parameter of type 'undefined'.
:
// TS_ERROR: Argument of type '"once"' is not assignable to parameter of type 'undefined'.
extender([{ a: 1 }, { a: 2 }], (row) => ({ once: 1, more: row.a * 2 }), 'once')
What am I missed?
In your example, T
is constrained by object
(which actually represents all non-primitives ). The type of "any plain object" can be represented as Record<PropertyKey, any>
(which uses the type utility Record<Keys, Type>
). PropertyKey
is an alias for string | number | symbol
string | number | symbol
string | number | symbol
, so this type means "any object with keys which are strings, numbers, and/or symbols, with values at those properties that can be any type".
By simply replacing that constraint, your code should compile without a diagnostic error.
The current return type of your function has some loss of type information if the input array has members which aren't exactly the Row
type (rather, they extend it), and it doesn't acknowledge the notion that T
could overwrite the type of property a
during the process of object merging in the map operation. Mapped types can help you preserve this kind of type information, and even preserve things like the length of an input array with a specifically-defined number of elements (a tuple). Consider the following example:
type PlainObject = Record<PropertyKey, any>;
type Mutable<T extends PlainObject> = { -readonly [K in keyof T]: T[K] };
type ExtendedTuple<A extends readonly PlainObject[], T extends PlainObject> = {
-readonly [Index in keyof A]: Mutable<Omit<A[Index], keyof T>> & Mutable<T>;
}
type Row = { a: number };
export function extender <
Data extends readonly Row[],
T extends PlainObject,
Prop extends keyof T,
>(data: Data, extend: (row: Data[number]) => T, newProp?: Prop): ExtendedTuple<Data, T> {
return data.map(row => ({ ...row, ...extend(row) })) as unknown as ExtendedTuple<Data, T>;
}
const extended1 = extender(
[{ a: 1 }, { a: 2 }],
() => ({ once: 1, more: 2 }),
'once',
); // Record<'a' | 'more' | 'once', number>[]
const extended2 = extender(
[{ a: 1 }, { a: 2 }],
(row) => ({ once: 1, more: row.a * 2 }),
'once',
); // Record<'a' | 'more' | 'once', number>[]
const [
e2o0, // Record<'a' | 'more' | 'once', number> | undefined
e2o1, // Record<'a' | 'more' | 'once', number> | undefined
e2o2, // Record<'a' | 'more' | 'once', number> | undefined
e2o3, // Record<'a' | 'more' | 'once', number> | undefined
e2o4, // Record<'a' | 'more' | 'once', number> | undefined
// etc...
] = extended2;
// Tuple correctness:
const extended3 = extender(
[{ a: 1, b: 1 }, { a: 2, b: 2 }, { a: 3, b: 3 }] as [Record<'a' | 'b', number>, Record<'a' | 'b', number>, Record<'a' | 'b', number>],
(row) => ({ a: 'hi', once: 1, more: row.a * 2 }),
'once',
); /* [
{ a: string } & Record<'b' | 'more' | 'once', number>,
{ a: string } & Record<'b' | 'more' | 'once', number>,
{ a: string } & Record<'b' | 'more' | 'once', number>,
] */
// Note the values at the tuple indexes are not potentially undefined:
const [
e3o0, // { a: string } & Record<'b' | 'more' | 'once', number>
e3o1, // { a: string } & Record<'b' | 'more' | 'once', number>
e3o2, // { a: string } & Record<'b' | 'more' | 'once', number>
e3o3, /* This error is expected
~~
Tuple type '[...]' of length '3' has no element at index '3'.(2493) */
] = extended3;
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.