简体   繁体   中英

typescript: validate function argument based on callback return type

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?

Summary

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.


More

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:

TS Playground

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM