简体   繁体   English

Typescript-isEmpty函数的通用类型保护

[英]Typescript - generic type guard for isEmpty function

I cannot properly implement generic isEmpty(value) in terms of narrowing type constrain down of provided value to it's empty counterpart. 我无法根据缩小类型将提供的值限制为空的对应项来正确实现泛型isEmpty(value)

Use case: 用例:

function getCountryNameById(countries: LookupItem[] = [], countryId?: number): string | undefined {

  if (isEmpty(countries) || !isNumber(countryId)) {

    // within this branch I would like to get countries argument to be narrowed to empty array type. 
    // Same would apply for other function which can have argument type of object or string. Why ? -> to prevent someone to do some mad code hacks like accessing non existent value from empty array ( which would happen on runtime ofc ) on compile time
    // $ExpectType []
    console.log(countries)

    return
  }

  // continue with code logic ...
  // implementation ...
}

similar case on constraining object: 约束对象的类似情况:

function doSomethingWithObject( data: { foo: string; bar: number } | object ){ 
   if(isEmpty(data)){
     // $ExpectType {}
     data

     // following should throw compile error, as data is empty object
     data.foo.toUpercase()

     return
   }

   // here we are sure that data is not empty on both runtime and compile time
}

isEmpty type guard implementation: isEmpty类型保护实现:

export const isEmpty = <T extends AllowedEmptyCheckTypes>(
  value: T | AllowedEmptyCheckTypes
): value is Empty<T> => {
  if (isBlank(value)) {
    return true
  }

  if (isString(value) || isArray(value)) {
    return value.length === 0
  }

  if (isObject(value)) {
    return Object.keys(value).length === 0
  }

  throw new Error(
    `checked value must be type of string | array | object. You provided ${typeof value}`
  )
}

With defined types: 具有定义的类型:

type EmptyArray = Array<never>
type Blank = null | undefined | void

/**
 * // object collects {} and Array<any> so adding both {} and Array<any> is not needed
 * @private
 */
export type AllowedEmptyCheckTypes = Blank | string | object

/**
 * Empty mapped type that will cast any AllowedEmptyCheckTypes to empty equivalent
 * @private
 */
export type Empty<T extends AllowedEmptyCheckTypes> = T extends string
  ? ''
  : T extends any[]
    ? EmptyArray
    : T extends object ? {} : T extends Blank ? T : never

It's kinda strange as it's narrowed correctly from type perspective, but not within if/else branch: 这有点奇怪,因为它从类型角度正确地缩小了,但是不在if / else分支中:

isEmpty for string values 字符串值的isEmpty

isEmpty for array values isEmpty用于数组值

isEmpty for object values isEmpty用于对象值

code can be seen here: https://github.com/Hotell/rex-tils/pull/13/files#diff-a3cdcb321a05315fcfc3309031eab1d8R177 可以在这里看到代码: https : //github.com/Hotell/rex-tils/pull/13/files#diff-a3cdcb321a05315fcfc3309031eab1d8R177

Related question: Type Guard for empty object 相关问题: 为空对象键入Guard

One way of handling this problem is by separating null checks ( undefined , null ) from empty value checks ( '' , [] {} ). 处理此问题的一种方法是将空值检查( undefinednull )与空值检查( ''[] {} )分开。 I tend to use two type guards for that — isDefined and isEmpty . 我倾向于为此使用两种类型的保护isDefinedisEmpty

First one may look like this. 第一个可能看起来像这样。 Note the typeof check — this makes it work with undeclared variables as well. 注意typeof检查-这也使其与未声明的变量一起使用。

function isDefined<T>(value: T | undefined | null): value is T {
  return (typeof value !== 'undefined') && (value !== null);
}

For empty values, the following model can be used. 对于空值,可以使用以下模型。

namespace Empty {
  export type String = '';
  export type Object = Record<string, never>;
  export type Array = never[];
}

type Empty =
  | Empty.Array
  | Empty.Object
  | Empty.String;

function isEmpty<T extends string | any[] | object>(subject: T | Empty): subject is Bottom<T> {
  switch (typeof subject) {
    case 'object':
      return (Object.keys(subject).length === 0);
    case 'string':
      return (subject === '');
    default:
      return false;
  }
}

type Bottom<T> =
  T extends string
    ? Empty.String
    : T extends any[]
        ? Empty.Array
        : T extends object
            ? Empty.Object
            : never;

The bottom values are inferred correctly. 可以正确推断最低值。

declare const foo: 'hello' | Empty.String;
declare const bar: [number, number] | Empty.Array;
declare const baz: Window | Empty.Object;

if (isEmpty(foo) && isEmpty(bar) && isEmpty(baz)) {
  console.log(foo, bar, baz);
}

Edit: added constraints on T as suggested. 编辑:按照建议在T上添加约束。

Here 这里

if (isEmpty(countries) || !isNumber(countryId)) {

you have two conditions, only one of which is a type guard for countries , that's why the type of countries doesn't change inside the if. 你有两个条件,只有其中之一是对一类后卫countries ,这就是为什么类型countries没有在里面是否改变。

As for objects, {} doesn't represent an empty object. 对于对象, {}并不代表空对象。 Anything, except for null and undefined , can be assigned to a variable of type {} . nullundefined外,任何其他内容都可以分配给{}类型的变量。 You might want to use { [prop: string]: never } or { [prop: string]: undefined } instead. 您可能想使用{ [prop: string]: never }{ [prop: string]: undefined }

So after a few twitter discussions and long SO/Github search I ended up with following solution: 因此,经过一些推特讨论和长时间的SO / Github搜索之后,我得到了以下解决方案:

  • first of all checking for null/undefined within a isEmpty doesn't make much sense ( although lodash.isEmpty handles that, IMHO it's doing too much and not in very explicit way ) 首先,在isEmpty中检查null / undefined并没有多大意义(尽管lodash.isEmpty处理了这一点,恕我直言,它做得太多而且不是很明确的方式)
  • because there is basically no difference between {} | 因为{} |之间基本上没有区别 object | object any[] the type guard narrowing would never work as expected any[]类型防护范围缩小将无法按预期工作
  • final solution accepts only valid values to check as arguments -> js objects and string and guard returns never so matching values would be type of never because anyway, it doesn't make any sense to execute any further login within if(isEmpty(value)){ ... } statement rather then terminate the program or throw an Error 最终解决方案仅接受有效值作为参数进行检查-> js对象和字符串,并且guard never返回,因此匹配的值将nevernever类型,因为无论如何,在if(isEmpty(value)){ ... }执行任何进一步的登录都没有任何意义if(isEmpty(value)){ ... }语句,而不是终止程序或引发错误

here is the final implementation: 这是最终的实现:

const isEmpty = <T extends string | object | any[]>(
  value: T
): value is never => {
  if (isString(value) || isArray(value)) {
    return value.length === 0
  }
   if (isObject(value)) {
    return Object.keys(value).length === 0
  }
   throw new Error(
    `checked value must be type of string | array | object. You provided ${
      // tslint:disable-next-line:strict-type-predicates
      value === null ? 'null' : typeof value
    }`
  )
}

https://github.com/Hotell/rex-tils/pull/13/files#diff-68ff3b6b6a1354b7277dfc4b23d99901R50 https://github.com/Hotell/rex-tils/pull/13/files#diff-68ff3b6b6a1354b7277dfc4b23d99901R50

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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