Typescript有条件地使某些属性成为可选的



我目前正试图使一个实用程序类型来展开sniptt monads选项。下面是我到目前为止的代码:

export interface Option<T> {
type: symbol;
isSome(): boolean;
isNone(): boolean;
match<U>(fn: Match<T, U>): U;
map<U>(fn: (val: T) => U): Option<U>;
andThen<U>(fn: (val: T) => Option<U>): Option<U>;
or<U>(optb: Option<U>): Option<T | U>;
and<U>(optb: Option<U>): Option<U>;
unwrapOr(def: T): T;
unwrap(): T | never;
}
export type UnwrappedOptionsType<T> = T extends (infer U)[]
? UnwrappedOptionsType<U>[]
: T extends object
? {
[P in keyof T]: T[P] extends Option<infer R>
? UnwrappedOptionsType<R> | undefined
: UnwrappedOptionsType<T[P]>;
}
: T;

我期望发生的是,类型被推断出来,属性是可选的选项。假设我有以下类型:

type SignUpRequest = {
username: string;
password: string;
email: Option<string>;
}

当我使用UnwrappedOptionsType<SignUpRequest>时,我希望得到以下类型:

{
username: string;
password: string;
email?: string | undefined;
}

结果是:

{
username: string;
password: string;
email: string;
}

它能够成功地推断出选项的类型,但它从来没有使它也接受undefined。如何使选项可选?

编辑修改代码,使示例可重现。此外,我特别希望属性是可选的,而不仅仅是可能未定义的。

这里有一个可能的方法:

type UnwrapOptions<T> =
T extends Option<infer U> ? UnwrapOptions<U> | undefined :
T extends readonly any[] ? {[I in keyof T]: UnwrapOptions<T[I]>} :
T extends object ? (
{ [K in keyof T as Option<any> extends T[K] ? never : K]: UnwrapOptions<T[K]> } &
{ [K in keyof T as Option<any> extends T[K] ? K : never]?: UnwrapOptions<T[K]> }
) extends infer U ? { [K in keyof U]: U[K] } : never :
T;

这是一个递归条件类型,因此,必然会有很多边缘情况,它会以令人惊讶或不希望的方式运行。所以在你使用这个或其他东西之前,你应该确保做大量的测试。

好的,让我们检查一下。如果计算UnwrapOptions<T>,其中T为…

  • …对于某些类型UOption<U>,然后我们递归计算UnwrapOptions<U>(如果您可能嵌套Option),并将其与undefined联合返回。这个undefined可能只有在Option是顶级的情况下才需要。也就是说,UnwrapOptions<Option<A>>应该是UnwrapOptions<A> | undefined;

  • …数组或元组类型,然后做一个简单的映射数组/元组类型,其中UnwrapOptions应用于每个数字元素。即UnwrapOptions<[A, B]>应为[UnwrapOptions<A>, UnwrapOptions<B>];

  • …一个基本类型,然后返回该基本类型。也就是说,UnwrapOptions<string>应该是string,等等;

  • …一个非数组对象类型,那么我们需要将对象拆分为其Option可分配属性和其非Option可分配属性。对于那些不能Option赋值的属性,我们只做一个直接映射类型,其中UnwrapOptions应用于每个属性。对于那些Option可赋值的,我们做同样的事情,但是使用?映射修饰符使所有属性都是可选的。然后我们要用一个交点把这两半连接起来。这足以给出你想要的类型,但交集可能很难看;因为像{a: 0} & {b: 1}这样的对象类型的交集相当于像{a: 0; b: 1}这样的单个对象类型,所以我使用条件类型推断将交集复制到一个新的类型参数U中,然后做一个无操作映射类型将交集连接到一个类型中。


好的,让我们测试一下。

type SignUpRequest = {
username: string;
password: string;
email: Option<string>;
}
type UnwrappedSignupRequest = UnwrapOptions<SignUpRequest>;
/* type UnwrappedSignupRequest = {
username: string;
password: string;
email?: string | undefined;
} */

这就是你想要的。如果我们取一个更复杂的类型呢?

interface Foo {
a: string;
b?: number;
c: string[];
d: { z?: string };
e: Option<number>;
f: Option<string>[];
g: Option<string> | number;
h: [1, Option<2>, 3];
i: { y: Option<string> };
j: Option<{ x: Option<{ w: string }> }>;
k: Foo;
}

,

type UnwrappedFoo = UnwrapOptions<Foo>;
/* type UnwrappedFoo = {
a: string;
b?: number | undefined;
c: string[];
d: {
z?: string | undefined;
};
f: (string | undefined)[];
h: [1, 2 | undefined, 3];
i: {
y?: string | undefined;
};
k: any; // <-- this displays as any but it is not
e?: number | undefined;
g?: string | number | undefined;
j?: {
x?: {
w: string;
} | undefined;
} | undefined;
} */

我认为这是合理的。唯一令人困惑的是,IntelliSense将递归部分(k属性)显示为any。但它确实会跟踪那个类型,你可以在检查属性时看到:

declare const uFoo: UnwrappedFoo;
uFoo.k;
/* (property) k: {
a: string;
b?: number | undefined;
c: string[];
d: {
z?: string | undefined;
};
f: (string | undefined)[];
h: [1, 2 | undefined, 3];
i: {
y?: string | undefined;
};
k: any;
e?: number | undefined;
g?: string | number | undefined;
j?: {
x?: {
w: string;
} | undefined;
} | undefined;
} */

就是这样,我觉得看起来不错。但是你应该测试它并相应地更新。


Playground链接到代码

您需要为UnwrappedOptionsType类型添加一个额外的类型保护以使该选项成为可选的:

type UnwrappedOptionsType<T> = T extends (infer U)[]
? UnwrappedOptionsType<U>[]
: T extends object
? {
[P in keyof T]: T[P] extends Option<infer R>
? (UnwrappedOptionsType<R> | undefined) | undefined
: UnwrappedOptionsType<T[P]>;
}
: T;

添加了类型保护后,SignUpRequest类型的UnwrappedOptionsType将为:

type UnwrappedOptionsType<SignUpRequest> = {
username: string;
password: string;
email?: (string | undefined) | undefined;
}

输出如下所示:

{
username: string;
password: string;
email?: string | undefined;
}

这就是你想要的吗?

最新更新