如何键入此合并函数?

时间:2019-01-25 17:00:18

标签: typescript

对于TypeScript或其他类型的语言中的标准类型以外的任何东西,我还是比较陌生,因此,我试图找出一种键入此函数的好方法。我想摆脱参数的任何类型和返回类型。

该函数返回的第一个值不为null,未定义或NaN。

true

这是您将如何使用它:

var output = ['d']

2 个答案:

答案 0 :(得分:2)

最简单的版本是捕获我们通过类型返回的参数类型的类型,以所有参数类型的并集形式返回。

我们可以在rest parameters中使用元组将所有参数的类型捕获为元组类型,而在type queries中使用元组来获取元组中所有类型的并集:

const coalesce = <T extends any[]>(...args: T): T[number] => {
    for (let i = 0; i < args.length; i++) {
        // args[i] === args[i] is to avoid NaN, because NaN !== NaN
        if (args[i] != null && args[i] === args[i]) {
            return args[i];
        }
    }
    return null;
};


// o is  string | number | null | undefined under strict null checks
// T is [null, undefined, number, string] so T[number] is string | number | null | undefined
let o = coalesce(null, undefined, NaN, 'maybe')  

现在,此功能的全部要点是删除null和undefine(也是NaN,但这些类型未在类型系统中建模,因此与它们无关)。当前签名将nullundefined留在类型中。如果任何一个参数不能为nullundefined(如果一个参数不是),我们可以添加一些映射类型和条件类型来删除nullundefined nullundefined将会返回)

type ExcludeNullIfAnyNotNullHelper<T> = {
    [P in keyof T]-?: (null extends T[P] ? never : null) |
        (undefined extends T[P] ? never : undefined) 
}[keyof T]
type ExcludeNullIfAnyNotNull<T extends any[]> = Exclude<T[number], ExcludeNullIfAnyNotNullHelper<T>>

type t1 = ExcludeNullIfAnyNotNull<[null, undefined, number, string]> // number | string
type t2 = ExcludeNullIfAnyNotNull<[null | string, string]> // string
type t3 = ExcludeNullIfAnyNotNull<[null | undefined | string, string]> // string
type t4 = ExcludeNullIfAnyNotNull<[null | undefined | string, string | null]> // string | null
type t5 = ExcludeNullIfAnyNotNull<[null | undefined | string, string | undefined]> // string | undefined
type t6 = ExcludeNullIfAnyNotNull<[null| undefined | string, string | undefined, number]> // string | number

function coalesce<T extends any[]>(...args: T): ExcludeNullIfAnyNotNull<T>
function coalesce<T extends any[]>(...args: T): T[number] {
    for (let i = 0; i < args.length; i++) {
        // args[i] === args[i] is to avoid NaN, because NaN !== NaN
        if (args[i] != null && args[i] === args[i]) {
            return args[i];
        }
    }
    return null;
};


// o is  string | number 
let o = coalesce(null, undefined, NaN, 'maybe')

答案 1 :(得分:1)

好的,所以这实际上是您要描述的一个相当复杂的类型函数。


首先,NaN出现了问题。 TypeScript没有NaN as a literal type的表示形式。值NaN的类型为number。因此,当您看到类型为number的值时,它可能NaN,编译器无法采取任何措施来防止这种情况。因此,当我看到number参数时,出于number | null的目的,我将不得不将其视为coalesce()之类的东西。所以这个:

function hmm(n: number) { return coalesce(n, "oops"); }

应该返回类型为number | "oops"的值,而不仅仅是number。通过明确认识到数字文字不能NaN,我可以对其进行一些改进,以便以下内容

coalesce(123, "oops");

应求值为123而不是123 | "oops",因为123是一个已知的非NaN数字。


因此,如果您处理coalesce() as a tuple的参数,那么您想要的是从左到右遍历元组,在元组中建立类型的并集,剥离所有可能的类型nullundefined类型。如果您遇到的类型绝对不是nullundefinednumber,则可以停止。否则,您会跑到最后,将null添加到您的联合中,然后停止。

此类型自然是递归的,通过TypeScript not currently supported。有很多方法可以解决此问题,但是我确定唯一可以处理的方法是将递归类型展开到某个固定深度,然后再进行纾困。

让我们开始编写类型:

// Head<L>: return the first element of a tuple L
// Head<[string, number, boolean]> ~ string
type Head<L extends any[]> = ((...l: L) => void) extends
  ((h: infer H, ...t: infer T) => void) ? H : never;

// Tail<L>: return the tuple L with the first element removed
// Tail<[string, number, boolean]> ~ [number, boolean]
type Tail<L extends any[]> = ((...l: L) => void) extends
  ((h: infer H, ...t: infer T) => void) ? T : never;

// MightSkip<T>: return unknown if coalesce() might possibly skip a value 
//  of this type; return never if coalesce() will definitely stop at a value
//  of this type.  
// MightSkip<string | null> ~ unknown (might be null)
// MightSkip<string> ~ never
// MightSkip<number> ~ unknown (it might skip because NaN)
// MightSkip<123> ~ never (it knows 123 is not NaN)
type MightSkip<T> =
  null extends T ? unknown :
  undefined extends T ? unknown :
  [T] extends [number] ? (number extends T ? unknown : never) :
  number extends T ? unknown :
  never;

我想要的递归类型函数是这样的:

/* error, can't do this
type FirstNonNull<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull<Tail<T>> : Head<T>;
*/

但是它不起作用,因为FirstNonNull是圆形的。如果可以编写,它的行为将是这样的:如果参数元组T为空,则返回null(如果coalesce()到达其参数列表的末尾,则会发生这种情况)。否则,如果第一个参数Head<T>可能被coalesce()跳过,则您将NonNullable<Head<T>>收集起来(删除了nullundefined)并加入了并通过调用FirstNonNull上的Tail<T>。否则,您仅收集Head<T>并停止。

我们不能做循环类型,所以让我们展开这样的类型:

type FirstNonNull<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull0<Tail<T>> : Head<T>;
type FirstNonNull0<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull1<Tail<T>> : Head<T>;
type FirstNonNull1<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull2<Tail<T>> : Head<T>;
type FirstNonNull2<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull3<Tail<T>> : Head<T>;
type FirstNonNull3<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull4<Tail<T>> : Head<T>;
type FirstNonNull4<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull5<Tail<T>> : Head<T>;
type FirstNonNull5<T extends any[]> = T['length'] extends 0 ? null :
  unknown extends MightSkip<Head<T>> ? NonNullable<Head<T>> | FirstNonNull6<Tail<T>> : Head<T>;
// as many as you need, and then bail out here:
type FirstNonNull6<T extends any[]> = NonNullable<T[number]> | null;

请注意,每种FirstNonNull1FirstNonNull2等类型如何仅引用下一个,因此不再循环。上面的方法适用于长度最大为7左右的任何元组,然后使用其余类型的并集在更长的参数列表上进行保存。如果您希望使用更长的参数列表,则可以轻松扩展上述方案。


几乎存在:为了捕获像123这样的文字类型的参数而又不会自动将它们扩展为像number这样的非文字类型的参数,我们需要give the compiler a hint,使用类似这样的类型Narrowable

type Narrowable = string | number | boolean | symbol | object |
  null | undefined | void | ((...args: any[]) => any) | {};

最后,让我们键入coalesce()并查看编译器告诉我们的内容:

/** returns the first not-null value excluding NaN */
const coalesce = <T extends Narrowable[]>(...args: T): FirstNonNull<T> => {
  for (let i = 0; i < args.length; i++) {
    // args[i] === args[i] is to avoid NaN, because NaN !== NaN
    if (args[i] != null && args[i] === args[i]) {
      return args[i] as any; // assert
    }
  }
  return null as any; // assert
};

是的,coalesce()的实现需要对返回值进行断言,因为编译器很难验证任何值是否为FirstNonNull<T>类型。可以使用overloads代替断言来完成;这取决于你。并注意如何将T的元素限制为Narrowable,以便推断文字。什么都没有:

const v0 = coalesce(null, undefined, "hey", "you"); 
// const v0: "hey"

// TypeScript doesn't have a NaN literal, so NaN is treated as number
const v1 = coalesce(null, undefined, NaN, 'maybe'); 
// const v1: number | "maybe"

const coinFlip = () => Math.random() < 0.5
const v2 = coalesce(coinFlip() ? "hey" : null, coinFlip() ? "you" : undefined, 123);
// const v2: "hey" | "you" | 123

const v3 = coalesce(undefined, undefined)
// const v3: null;

这看起来和我想象的一样好。 v0只是"hey",而不是"hey" | "you",因为coalesce()肯定会停在"hey"处。 v1number | "maybe",因为编译器将NaN视为number,所以没有比这更好的了。 v2"hey" | "you" | 123,因为前两个参数中的任何一个都可能为null,但是123绝对不是,因此它必须是这三个参数之一。并且v3null,因为它不在参数列表的末尾。


好多了。就像我说的,在类型系统中描述您要做的事情相当复杂。上面的内容也许对您有用。好的,祝你好运!