对于TypeScript或其他类型的语言中的标准类型以外的任何东西,我还是比较陌生,因此,我试图找出一种键入此函数的好方法。我想摆脱参数的任何类型和返回类型。
该函数返回的第一个值不为null,未定义或NaN。
true
这是您将如何使用它:
var output = ['d']
答案 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
,但这些类型未在类型系统中建模,因此与它们无关)。当前签名将null
和undefined
留在类型中。如果任何一个参数不能为null
或undefined
(如果一个参数不是),我们可以添加一些映射类型和条件类型来删除null
和undefined
null
或undefined
将会返回)
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的参数,那么您想要的是从左到右遍历元组,在元组中建立类型的并集,剥离所有可能的类型null
或undefined
类型。如果您遇到的类型绝对不是null
,undefined
或number
,则可以停止。否则,您会跑到最后,将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>>
收集起来(删除了null
或undefined
)并加入了并通过调用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;
请注意,每种FirstNonNull1
,FirstNonNull2
等类型如何仅引用下一个,因此不再循环。上面的方法适用于长度最大为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"
处。 v1
是number | "maybe"
,因为编译器将NaN
视为number
,所以没有比这更好的了。 v2
是"hey" | "you" | 123
,因为前两个参数中的任何一个都可能为null,但是123
绝对不是,因此它必须是这三个参数之一。并且v3
是null
,因为它不在参数列表的末尾。
好多了。就像我说的,在类型系统中描述您要做的事情相当复杂。上面的内容也许对您有用。好的,祝你好运!