在TypeScript中使用带有可选字段的对象的const断言数组的正确方法



我有这样的数据:

const data = [
{ id: 1, a: "foo" },
{ id: 2 },
] as const;

我可以";验证";它与现有的类型:

type Row = {
readonly id: number;
readonly a?: string;
};
// This will explode if data has { b: "bar" }
const dataValidated: Readonly<Row[]> = data;

但我得到了";类型上不存在属性"a"当我尝试访问它时:

data.map((x) => x.a);
//                ^ Error here

如果我使用dataValidated,我将失去as const:的类型窄化

dataValidated.map((x) => x.id);
//                         ^^
// number, but I want 1 | 2
dataValidated.map((x) => x.a);
//                         ^
// string | undefined, but I want "foo" | undefined

我尝试过一个简单的交集,它对id有帮助,但对a:没有帮助

const dataIntersected: Readonly<(Row & typeof data[number])[]> = data;
dataIntersected.map((x) => x.id);
//                           ^^
// Good, it's 1 | 2 now
dataIntersected.map((x) => x.a);
//                           ^
// Still string | undefined

如何访问所有字段而不进行类型扩展或更改数据(例如,在第二项中添加冗余a: undefined(?

相关:打字:联合类型到可选值的深度交集(难度:95+级(

映射类型!

const dataSolved: {
[K in keyof Row]: Extract<
typeof data[number],
Pick<Row, K>
>[K]
} = data;
dataSolved.map((x) => x.id);
//                      ^^
// 1 | 2
dataSolved.map((x) => x.a);
//                      ^
// "foo" | undefined

{ [K in keyof Row]: ... }逐个遍历Row的所有字段;指定";至CCD_ 8。

typeof data[number]只是所有文字行类型的并集,在这种情况下与data相同。

Pick<Row, K>丢弃Row中除K之外的所有字段,因此对于K = 'a',结果为{ readonly a?: string }

Extract从上面提到的联合中丢弃了Pick<Row, K>不匹配的所有类型,所以对于K = 'a',结果是{ id: 1, a: "foo" }

Extract之后的[K]创建了字段K的并集,该字段在Extract返回的并集中是公共的,因此对于K = 'a',它是"foo"

CCD_ 26和CCD_;选择性;保持不变,默认情况下贴图不会接触到它们。

为了简洁起见,这是一个基本示例。该解决方案更多地涉及嵌套结构,但对于递归条件类型和分布性仍然是可能的:

type Primitive = string | number | boolean | null | undefined;
type Combine<T extends U, U> = T extends Readonly<Primitive | Primitive[]>
? T
: T extends ReadonlyArray<infer I>
? Readonly<Combine<I, Extract<U, Readonly<any[]>>[number]>[]>
: U extends Readonly<Primitive | any[]>
? never
: string extends keyof U
? { [K in keyof T & string]: Combine<T[K], U[K]> }
: number extends keyof U
? { [K in keyof T & number]: Combine<T[K], U[K]> }
: symbol extends keyof U
? { [K in keyof T & symbol]: Combine<T[K], U[K]> }
: { [K in keyof U]: Combine<T[K], U[K]> };
const combined: Combine<typeof data, Readonly<Row[]>> = data;

与这个问题有一定关系的是,也可以使所有部分存在的字段都是可选的,而不使用";模式";的数据(不需要Row(,具有密钥重映射:

type AllKeys<T> = T extends any ? keyof T : never;
type WithKey<T, K extends keyof T> = Extract<T, { [_ in K]: any }>[K];
type OptionalizeHelper<T> = [T] extends [Readonly<Primitive | Primitive[]>]
? T
: [T] extends [ReadonlyArray<infer I>]
? Readonly<Optionalize<I>[]>
: {
[K in AllKeys<T> as K extends keyof T ? K : never]: Optionalize<
WithKey<T, K>
>;
} & {
[K in AllKeys<T> as K extends keyof T ? never : K]?: Optionalize<
WithKey<T, K>
>;
};
type Optionalize<T> =
| OptionalizeHelper<Extract<T, Readonly<Primitive | Primitive[]>>>
| OptionalizeHelper<Extract<T, Readonly<any[]>>>
| OptionalizeHelper<Exclude<T, Readonly<Primitive | any[]>>>;
const optionalized: Optionalize<typeof data> = data;

我创建了一个游乐场,通过类型测试演示了CombineOptionalize的用法。

我后来发现这个答案中的Widen类型非常接近Optionalize,但不能很好地处理元组和数组。

最新更新