如何将对象值提取为文字类型联合打字稿
How to extract object values as literal type union typescript
我正在实现一个 groupBy 函数,它基本上是这样的:
export const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
property: U,
): { [key in T[U]]: T[] } => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
return memo;
}, {} as { [key in T[U]]: Array<T> });
我知道打字不对,我把它搞得一团糟:
const data = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
];
const result = groupBy(data, 'group')
{
item: [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' }
],
animal: [ { name: 'snake', value: 2, group: 'animal' } ],
car: [
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' }
]
}
这是我能找到的最大类型安全,但我仍然在 T[U]
.
上遇到一些错误
Type 'T[U]' is not assignable to type 'string | number | symbol'.
而return类型如下:
const result: {
[x: string]: {
name: string;
value: number;
group: string;
}[];
}
有什么方法可以实现这种 return 类型:
const result: {
item: {
name: string;
value: number;
group: string;
}[];
animal: {
name: string;
value: number;
group: string;
}[];
car: {
name: string;
value: number;
group: string;
}[];
}
如果我可以将给定键的所有输入对象值提取为如下文字:
const test = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
] as const ;
type ValueAtKey = (typeof test)[number]['group']; // "item" | "animal" | "car"
但是我如何在通用函数中执行此操作 const asertion
?
做这样的事情:
type SomeMagicType<T extends Record<string, unknown>[], U extends keyof T[number]> = T[number][U];
export const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
property: U,
): { [key in SomeMagicType<T, U>]: T[] } => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
return memo;
}, {} as { [key in SomeMagicType<T, U>]: Array<T> });
如果不可能,那么我该如何消除上述错误。我不明白为什么 Type 'T[U]' is not assignable to type 'string | number | symbol'
if T extends Record<string, unknown>
.
首先,如果您希望 groupBy
的 return 类型具有特定的键,例如 item
、animal
和 car
,您将非常需要做类似 const
assertion when you initialize data
. Generally speaking, the compiler infers the type of a string-valued property to be string
and not the specific string literal type 的事情。因此 const x = {a: "b"}
中 x
的类型将被推断为 {a: string}
而不是 {a: "b"}
。由于 "item"
、"animal"
和 "car"
是 data
的字符串值属性,您需要做一些工作来保留它们的值。最简单的做法是:
const data = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
] as const;
尽管现在编译器会记住很多关于 data
:
的结构
/* const data: readonly [{
readonly name: "corn cob";
readonly value: 17;
readonly group: "item";
}, {
readonly name: "Dirty toilet";
readonly value: 6;
readonly group: "item";
}, {
readonly name: "snake";
readonly value: 2;
readonly group: "animal";
}, {
readonly name: "tesla";
readonly value: 17;
readonly group: "car";
}, {
readonly name: "gurgel";
readonly value: 23;
readonly group: "car";
}] */
而你只真正关心 group
的文字类型。你可以做一些更复杂的事情,比如
const data = [
{ name: 'corn cob', value: 17, group: 'item' as const },
{ name: 'Dirty toilet', value: 6, group: 'item' as const },
{ name: 'snake', value: 2, group: 'animal' as const },
{ name: 'tesla', value: 17, group: 'car' as const },
{ name: 'gurgel', value: 23, group: 'car' as const },
];
这导致
/* const data: ({
name: string;
value: number;
group: "item";
} | {
name: string;
value: number;
group: "animal";
} | {
name: string;
value: number;
group: "car";
})[] */
我猜哪个没那么难看。由你决定。
无论如何,一旦你有足够的强类型 data
,我们可以给 groupBy
一个正确使用它的调用签名。让我们开始吧:
declare const groupBy:
<T extends Record<string, PropertyKey>, K extends keyof T>(
objArr: readonly T[],
property: K,
) => Record<T[K], T[]>
这与您的版本相似(除了 readonly T[]
比 T[]
更宽松,并且从 U
重命名为更传统的 K
以表示“钥匙”)。这里重要的区别是我已经将 T
的值类型从 unknown
更改为 PropertyKey
一个 standard library defined type 等同于 string | number | symbol
,可用作键的类型.所以现在编译器知道 T[K]
本身是类似键的,并且可以在 Record<T[K], T[]>
.
中使用
当然我们并不是真的想说每个 属性 in T
都需要是keylike;我们只想将键 K
处的属性限制为该类型。所以我们可以重写调用签名:
declare const groupBy:
<T extends Record<K, PropertyKey>, K extends keyof T>(
objArr: readonly T[],
property: K,
) => Record<T[K], T[]>
现在 T
取决于 K
。我们可以将 K extends keyof T
的约束放宽到 K extends PropertyKey
,因为 T
的约束现在保证 K
是一个键。但这取决于你。
无论如何,现在这会起作用,但是当我们查看结果的类型时,那个巨大的 const
-asserted 类型真的会回到我们身边:
const resultOrig = groupBy(data, 'group');
/* const resultOrig: Record<"item" | "animal" | "car", ({
readonly name: "corn cob";
readonly value: 17;
readonly group: "item";
} | {
readonly name: "Dirty toilet";
readonly value: 6;
readonly group: "item";
} | {
readonly name: "snake";
readonly value: 2;
readonly group: "animal";
} | {
readonly name: "tesla";
readonly value: 17;
readonly group: "car";
} | {
readonly name: "gurgel";
readonly value: 23;
readonly group: "car";
})[]> */
那个类型是正确的,但你提到你想要一些不太具体的 属性 值。你可以让编译器计算出这样的类型,但这并不简单:
type WidenLiteral<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T;
declare const groupBy: <T extends Record<K, PropertyKey>, K extends PropertyKey>(
objArr: readonly T[], property: K) => Record<T[K], {
[P in PropertyKey & keyof T]: WidenLiteral<T[P]>;
}[]> */
我在那里所做的是编写一个类型 WidenLiteral<T>
,它将一个类型转换为扩展版本,其中任何 string
、number
或 boolean
文字类型分别扩展为 string
、number
和 boolean
。所以WidenLiteral<"a" | Date>
会变成string | Date
,WidenLiteral<1 | 2 | 3 | 4>
会变成number
.
然后,我 return 不是 return 数组 T[]
,而是数组 {[P in PropertyKey & keyof T]: WidenLiteral<T[P]>}
。它所做的是采用 T
类型,如果它是一个联合,它会将它折叠成一个单一类型(并忘记联合的任何一个成员不存在的任何属性),然后它扩大文字WidenLiteral
的属性。所以如果输入是{a: 0, b: "a", c: true} | {a: 1, b: "b", d: false}
,输出就是{a: number, b: string}
.
请注意,当 mapped type over T
is not 时会自动发生这种情况,因此 {[P in keyof T]: 0}
是同态的,并且会将 {a: 1, c: 1} | {a: 2, d: 2}
变成 {a: 0, c: 0} | {a: 0, d: 0}
但 {[P in (PropertyKey & keyof T)]: 0}
不是同态的并且会将 {a: 1, c: 1} | {a: 2, d: 2}
变成 {a: 0}
.
无论如何,这个调用签名会把 data
的乱七八糟的类型变成更令人愉快的东西:
const result = groupBy(data, 'group');
/* const result: Record<"item" | "animal" | "car", {
group: string;
name: string;
value: number;
}[]> */
太棒了!
哦,当然 groupBy
的实现至少需要一个 type assertion:
const groupBy = <T extends Record<K, PropertyKey>, K extends PropertyKey>(
objArr: readonly T[],
property: K,
) => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x as any); // <-- here
return memo;
}, {} as Record<T[K], { [P in (PropertyKey & keyof T)]: WidenLiteral<T[P]> }[]>);
这里需要类型断言,因为虽然 x
显然是 T
类型的值,但它不是 {[P in ...]: WidenLiteral<...>}
类型的值。好吧,反正不是编译器。编译器不擅长对尚未指定的泛型类型进行高阶推理,例如 groupBy
.
主体中的 T
所以从实现者的角度来看,这有点不安全,你需要确保你写得正确。编译器无法真正捕捉到类型断言的所有错误。所以 push(x as any)
和 push(123 as any)
在编译器看来是一样的。
但是从调用者的角度来看,这应该能很好地工作(只要调用者使用特定类型而不是未指定的泛型)。
我正在实现一个 groupBy 函数,它基本上是这样的:
export const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
property: U,
): { [key in T[U]]: T[] } => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
return memo;
}, {} as { [key in T[U]]: Array<T> });
我知道打字不对,我把它搞得一团糟:
const data = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
];
const result = groupBy(data, 'group')
{
item: [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' }
],
animal: [ { name: 'snake', value: 2, group: 'animal' } ],
car: [
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' }
]
}
这是我能找到的最大类型安全,但我仍然在 T[U]
.
Type 'T[U]' is not assignable to type 'string | number | symbol'.
而return类型如下:
const result: {
[x: string]: {
name: string;
value: number;
group: string;
}[];
}
有什么方法可以实现这种 return 类型:
const result: {
item: {
name: string;
value: number;
group: string;
}[];
animal: {
name: string;
value: number;
group: string;
}[];
car: {
name: string;
value: number;
group: string;
}[];
}
如果我可以将给定键的所有输入对象值提取为如下文字:
const test = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
] as const ;
type ValueAtKey = (typeof test)[number]['group']; // "item" | "animal" | "car"
但是我如何在通用函数中执行此操作 const asertion
?
做这样的事情:
type SomeMagicType<T extends Record<string, unknown>[], U extends keyof T[number]> = T[number][U];
export const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
property: U,
): { [key in SomeMagicType<T, U>]: T[] } => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
return memo;
}, {} as { [key in SomeMagicType<T, U>]: Array<T> });
如果不可能,那么我该如何消除上述错误。我不明白为什么 Type 'T[U]' is not assignable to type 'string | number | symbol'
if T extends Record<string, unknown>
.
首先,如果您希望 groupBy
的 return 类型具有特定的键,例如 item
、animal
和 car
,您将非常需要做类似 const
assertion when you initialize data
. Generally speaking, the compiler infers the type of a string-valued property to be string
and not the specific string literal type 的事情。因此 const x = {a: "b"}
中 x
的类型将被推断为 {a: string}
而不是 {a: "b"}
。由于 "item"
、"animal"
和 "car"
是 data
的字符串值属性,您需要做一些工作来保留它们的值。最简单的做法是:
const data = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
] as const;
尽管现在编译器会记住很多关于 data
:
/* const data: readonly [{
readonly name: "corn cob";
readonly value: 17;
readonly group: "item";
}, {
readonly name: "Dirty toilet";
readonly value: 6;
readonly group: "item";
}, {
readonly name: "snake";
readonly value: 2;
readonly group: "animal";
}, {
readonly name: "tesla";
readonly value: 17;
readonly group: "car";
}, {
readonly name: "gurgel";
readonly value: 23;
readonly group: "car";
}] */
而你只真正关心 group
的文字类型。你可以做一些更复杂的事情,比如
const data = [
{ name: 'corn cob', value: 17, group: 'item' as const },
{ name: 'Dirty toilet', value: 6, group: 'item' as const },
{ name: 'snake', value: 2, group: 'animal' as const },
{ name: 'tesla', value: 17, group: 'car' as const },
{ name: 'gurgel', value: 23, group: 'car' as const },
];
这导致
/* const data: ({
name: string;
value: number;
group: "item";
} | {
name: string;
value: number;
group: "animal";
} | {
name: string;
value: number;
group: "car";
})[] */
我猜哪个没那么难看。由你决定。
无论如何,一旦你有足够的强类型 data
,我们可以给 groupBy
一个正确使用它的调用签名。让我们开始吧:
declare const groupBy:
<T extends Record<string, PropertyKey>, K extends keyof T>(
objArr: readonly T[],
property: K,
) => Record<T[K], T[]>
这与您的版本相似(除了 readonly T[]
比 T[]
更宽松,并且从 U
重命名为更传统的 K
以表示“钥匙”)。这里重要的区别是我已经将 T
的值类型从 unknown
更改为 PropertyKey
一个 standard library defined type 等同于 string | number | symbol
,可用作键的类型.所以现在编译器知道 T[K]
本身是类似键的,并且可以在 Record<T[K], T[]>
.
当然我们并不是真的想说每个 属性 in T
都需要是keylike;我们只想将键 K
处的属性限制为该类型。所以我们可以重写调用签名:
declare const groupBy:
<T extends Record<K, PropertyKey>, K extends keyof T>(
objArr: readonly T[],
property: K,
) => Record<T[K], T[]>
现在 T
取决于 K
。我们可以将 K extends keyof T
的约束放宽到 K extends PropertyKey
,因为 T
的约束现在保证 K
是一个键。但这取决于你。
无论如何,现在这会起作用,但是当我们查看结果的类型时,那个巨大的 const
-asserted 类型真的会回到我们身边:
const resultOrig = groupBy(data, 'group');
/* const resultOrig: Record<"item" | "animal" | "car", ({
readonly name: "corn cob";
readonly value: 17;
readonly group: "item";
} | {
readonly name: "Dirty toilet";
readonly value: 6;
readonly group: "item";
} | {
readonly name: "snake";
readonly value: 2;
readonly group: "animal";
} | {
readonly name: "tesla";
readonly value: 17;
readonly group: "car";
} | {
readonly name: "gurgel";
readonly value: 23;
readonly group: "car";
})[]> */
那个类型是正确的,但你提到你想要一些不太具体的 属性 值。你可以让编译器计算出这样的类型,但这并不简单:
type WidenLiteral<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T;
declare const groupBy: <T extends Record<K, PropertyKey>, K extends PropertyKey>(
objArr: readonly T[], property: K) => Record<T[K], {
[P in PropertyKey & keyof T]: WidenLiteral<T[P]>;
}[]> */
我在那里所做的是编写一个类型 WidenLiteral<T>
,它将一个类型转换为扩展版本,其中任何 string
、number
或 boolean
文字类型分别扩展为 string
、number
和 boolean
。所以WidenLiteral<"a" | Date>
会变成string | Date
,WidenLiteral<1 | 2 | 3 | 4>
会变成number
.
然后,我 return 不是 return 数组 T[]
,而是数组 {[P in PropertyKey & keyof T]: WidenLiteral<T[P]>}
。它所做的是采用 T
类型,如果它是一个联合,它会将它折叠成一个单一类型(并忘记联合的任何一个成员不存在的任何属性),然后它扩大文字WidenLiteral
的属性。所以如果输入是{a: 0, b: "a", c: true} | {a: 1, b: "b", d: false}
,输出就是{a: number, b: string}
.
请注意,当 mapped type over T
is not {[P in keyof T]: 0}
是同态的,并且会将 {a: 1, c: 1} | {a: 2, d: 2}
变成 {a: 0, c: 0} | {a: 0, d: 0}
但 {[P in (PropertyKey & keyof T)]: 0}
不是同态的并且会将 {a: 1, c: 1} | {a: 2, d: 2}
变成 {a: 0}
.
无论如何,这个调用签名会把 data
的乱七八糟的类型变成更令人愉快的东西:
const result = groupBy(data, 'group');
/* const result: Record<"item" | "animal" | "car", {
group: string;
name: string;
value: number;
}[]> */
太棒了!
哦,当然 groupBy
的实现至少需要一个 type assertion:
const groupBy = <T extends Record<K, PropertyKey>, K extends PropertyKey>(
objArr: readonly T[],
property: K,
) => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x as any); // <-- here
return memo;
}, {} as Record<T[K], { [P in (PropertyKey & keyof T)]: WidenLiteral<T[P]> }[]>);
这里需要类型断言,因为虽然 x
显然是 T
类型的值,但它不是 {[P in ...]: WidenLiteral<...>}
类型的值。好吧,反正不是编译器。编译器不擅长对尚未指定的泛型类型进行高阶推理,例如 groupBy
.
T
所以从实现者的角度来看,这有点不安全,你需要确保你写得正确。编译器无法真正捕捉到类型断言的所有错误。所以 push(x as any)
和 push(123 as any)
在编译器看来是一样的。
但是从调用者的角度来看,这应该能很好地工作(只要调用者使用特定类型而不是未指定的泛型)。