TypeScript toSafeArray:键入一个将任何内容转换为有效数组的函数

TypeScript toSafeArray: type a function that converts anything into an valid array

如何键入一个函数,将 任何东西 转换为安全数组?

toArray(null) // []: []
toArray(undefined) // []: []
toArray([]) // []: []
toArray({}) // [{}]: {}[]
toArray(0) // [0]: number[]
toArray('') // ['']: string[]
toArray(['']) // ['']: string[]
toArray([1]) // [1]: number[]

我目前有:

function toArray<T>(
  value: T
): T extends null | undefined ? [] : T extends Array<infer U> ? U[] : [T] {
  if (value == null) {
    // It's invalid, return an empty array.
    const safeArray: [] = []
    return safeArray
  }

  if (Array.isArray(value)) {
    // It's already an array, return it unmodified.
    const safeArray: T[] = value
    return safeArray
  }

  // It's valid, but not an array, turn it into an array.
  const safeArray: [T] = [value]
  return safeArray
}

但是我 return 的任何东西,TypeScript 都会对我大喊大叫,说它不能分配给 return 类型。

之前我有:

declare function toArray<T>(value: T | Array<T>): Array<T>

但它错误地 returns null[]undefined[] 这是不希望的。

这是目前 TypeScript 的设计限制或缺失功能。请参阅 microsoft/TypeScript#33912 进行权威讨论。

TypeScript 编译器并不能真正对 conditional types that depend on as-yet unspecified generic 类型参数进行很多推理。这些类型对编译器来说本质​​上是不透明的,它无法验证特定值是否可以分配给它。

当您 调用 toArray() 时,类型参数 T 被指定为某种特定类型(通常这是由编译器推断的)然后return 类型 T extends null | undefined ? [] : ... 可以评估为某种特定类型本身。但是在toArray()body中,没有指定类型参数T。所以 T extends null | undefined ? [] : ... 只是一些“黑匣子”类型。您可以检查 value 是否为 null,但这不会缩小或指定类型参数 T,因此编译器无法知道值 [] 是否为可分配给 T extends null | undefined ? [] : ...。在每个 return 语句处都会出现编译器错误:

function toArray<T>(
  value: T
): T extends null | undefined ? [] : T extends ReadonlyArray<any> ? T : [T] {
  if (value == null) { return []; } // error!
  if (Array.isArray(value)) { return value; } // error!
  return [value]; // error!
}

除非 microsoft/TypeScript#33912 得到解决,否则此类通用条件类型的唯一可行方法是接受编译器无法验证实现是否满足调用签名,并负责这样验证自己。一种方法是在编译器无法验证可分配性的每个地方使用 type assertion

type ToArray<T> =
  T extends null | undefined ? [] :
  T extends ReadonlyArray<any> ? T :
  [T];

function toArray<T>(
  value: T
): ToArray<T> {
  if (value == null) { return [] as ToArray<T>; }
  if (Array.isArray(value)) { return value as ToArray<T>; }
  return [value] as ToArray<T>;
}

现在没有错误了。请注意,您需要确保做正确的事;您可以将 value == null 更改为 value != null 并且仍然不会出现错误。当您说 as ToArray<T> 时,编译器信任您,因此请确保不要对它撒谎。

无论如何,这可行但很乏味。我做了一个 type alias of ToArray<T> to prevent writing out that long type every time, or giving up and writing as any. In cases like this, I tend to not use type assertions and opt for single-call-signature overloads 代替:

// call signature
function toArray<T>(
  value: T
): T extends null | undefined ? [] : T extends ReadonlyArray<any> ? T : [T];

// implementation
function toArray(value: unknown) {
  if (value == null) { return []; }
  if (Array.isArray(value)) { return value; }
  return [value];
}

函数重载实现的检查比常规函数的实现更宽松。所以上面的编译没有错误。调用者只能看到一个调用签名,而允许更宽松的实现和 return 类型 unknown 的值。这与类型断言一样不安全(同样,!= null 不会被捕获),但实现更清晰,调用者看到的类型与实现看到的类型之间有更清晰的区分。

Playground link to code