从已区分联合数组中缩小 return 类型的查找
Narrow return type of find from discriminated union array
我经常使用如下例中的代码,我想知道是否有一些聪明的方法来键入 find
结果而无需进行显式类型断言。
type Foo = { type: "Foo" };
type Goo = { type: "Goo" };
type Union = Foo | Goo;
const arr: Union[] = [];
const foo = arr.find(a => a.type === "Foo") as Foo;
如果 as Foo
类型断言被省略,结果是 Union
类型,即使它只能 return 类型 Foo
.
在这些示例中,将 find
的类型固定为 return 缩小类型的最简洁方法是什么?
编辑:这个问题也适用于filter
和其他类似的方法。
Edit2:建议的类似问题的已接受答案 () 表明,通过在 find/filter
的谓词中使用类型保护,可以缩小 return 值的范围。
这个类型保护函数应该如何缩小任何有区别的联合,例如区分字符串文字总是在 type
键下?
如果你想要一个用于用户定义类型保护函数的生成器 returning 一个类型谓词来区分被区分的联合,它可能看起来像这样:
function discriminate<K extends PropertyKey, V extends string | number | boolean>(
discriminantKey: K, discriminantValue: V
) {
return <T extends Record<K, any>>(
obj: T & Record<K, V extends T[K] ? T[K] : V>
): obj is Extract<T, Record<K, V>> =>
obj[discriminantKey] === discriminantValue;
}
如果我调用 discriminate("type", "Foo")
,结果是一个签名类似于 <T>(obj: T)=>obj is Extract<T, {type: "Foo"}>
的函数。 (我说它是 similar 因为实际的 return 值将 T
限制为仅以 "type"
作为键和可以分配的值的类型 "Foo"
到。)让我们看看它是如何工作的:
const foo = arr.find(discriminate("type", "Foo")); // Foo | undefined
const goos = arr.filter(discriminate("type", "Goo")); // Goo[]
看起来不错。如果你通过不适用的 field/values:
会发生什么
const mistake1 = arr.find(discriminate("hype", "Foo")); // error!
// -------------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Union is not assignable to Record<"hype", any>.
const mistake2 = arr.find(discriminate("type", "Hoo")); // error!
// -------------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Union is not assignable to ((Foo | Goo) & Record<"type", "Hoo">)
好的,希望对您有所帮助;祝你好运!
这是来自上面答案的 jcalz 代码,扩展了否定和并集。
export function isDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
discriminantKey: K, discriminantValue: V | V[]
) {
return <T extends Record<K, any>>(
obj: T & Record<K, V extends T[K] ? T[K] : V>
): obj is Extract<T, Record<K, V>> =>
Array.isArray(discriminantValue)
? discriminantValue.some(v => obj[discriminantKey] === v)
: obj[discriminantKey] === discriminantValue;
}
export function isNotDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
discriminantKey: K, discriminantValue: V | V[]
) {
return <T extends Record<K, any>>(
obj: T & Record<K, V extends T[K] ? T[K] : V>
): obj is Exclude<T, Record<K, V>> =>
Array.isArray(discriminantValue)
? discriminantValue.some(v => obj[discriminantKey] === v)
: obj[discriminantKey] === discriminantValue;
}
和用法:
type A = { type: "A" };
type B = { type: "B" };
type C = { type: "C" };
type Union = A | B | C;
const arr: Union[] = [];
arr.find(isDiscriminate("type", "A")); // A
arr.find(isDiscriminate("type", ["A", "B"])); // A | B
arr.find(isNotDiscriminate("type", "A")); // B | C
arr.find(isNotDiscriminate("type", ["A", "B"])) // C
接受的答案很好,但很难理解。我做了这样的事情。它的可重用性较低,仅适用于一种类型,但更易于阅读。
function isType<V extends Union['type']>(val: V) {
return (obj: Union):
obj is Extract<Union, {type: V}> => obj.type === val;
}
const found = arr.find(isType('Foo'));
感谢 jcalz 。
我为 predicate guards 注释了一种稍微不同的工厂风格:
function hasProp<K extends PropertyKey, V extends string | number | boolean>(k: K, v: V) {
// All candidate types might have key `K` of any type
type Candidate = Partial<Record<K, any>> | null | undefined
// All matching subtypes of T must have key `K` equal value `V`
type Match<T extends Candidate> = Extract<T, Record<K, V>>
return <T extends Candidate>(obj: T): obj is Match<T> => (
obj?.[k] === v
)
}
我经常使用如下例中的代码,我想知道是否有一些聪明的方法来键入 find
结果而无需进行显式类型断言。
type Foo = { type: "Foo" };
type Goo = { type: "Goo" };
type Union = Foo | Goo;
const arr: Union[] = [];
const foo = arr.find(a => a.type === "Foo") as Foo;
如果 as Foo
类型断言被省略,结果是 Union
类型,即使它只能 return 类型 Foo
.
在这些示例中,将 find
的类型固定为 return 缩小类型的最简洁方法是什么?
编辑:这个问题也适用于filter
和其他类似的方法。
Edit2:建议的类似问题的已接受答案 (find/filter
的谓词中使用类型保护,可以缩小 return 值的范围。
这个类型保护函数应该如何缩小任何有区别的联合,例如区分字符串文字总是在 type
键下?
如果你想要一个用于用户定义类型保护函数的生成器 returning 一个类型谓词来区分被区分的联合,它可能看起来像这样:
function discriminate<K extends PropertyKey, V extends string | number | boolean>(
discriminantKey: K, discriminantValue: V
) {
return <T extends Record<K, any>>(
obj: T & Record<K, V extends T[K] ? T[K] : V>
): obj is Extract<T, Record<K, V>> =>
obj[discriminantKey] === discriminantValue;
}
如果我调用 discriminate("type", "Foo")
,结果是一个签名类似于 <T>(obj: T)=>obj is Extract<T, {type: "Foo"}>
的函数。 (我说它是 similar 因为实际的 return 值将 T
限制为仅以 "type"
作为键和可以分配的值的类型 "Foo"
到。)让我们看看它是如何工作的:
const foo = arr.find(discriminate("type", "Foo")); // Foo | undefined
const goos = arr.filter(discriminate("type", "Goo")); // Goo[]
看起来不错。如果你通过不适用的 field/values:
会发生什么const mistake1 = arr.find(discriminate("hype", "Foo")); // error!
// -------------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Union is not assignable to Record<"hype", any>.
const mistake2 = arr.find(discriminate("type", "Hoo")); // error!
// -------------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Union is not assignable to ((Foo | Goo) & Record<"type", "Hoo">)
好的,希望对您有所帮助;祝你好运!
这是来自上面答案的 jcalz 代码,扩展了否定和并集。
export function isDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
discriminantKey: K, discriminantValue: V | V[]
) {
return <T extends Record<K, any>>(
obj: T & Record<K, V extends T[K] ? T[K] : V>
): obj is Extract<T, Record<K, V>> =>
Array.isArray(discriminantValue)
? discriminantValue.some(v => obj[discriminantKey] === v)
: obj[discriminantKey] === discriminantValue;
}
export function isNotDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
discriminantKey: K, discriminantValue: V | V[]
) {
return <T extends Record<K, any>>(
obj: T & Record<K, V extends T[K] ? T[K] : V>
): obj is Exclude<T, Record<K, V>> =>
Array.isArray(discriminantValue)
? discriminantValue.some(v => obj[discriminantKey] === v)
: obj[discriminantKey] === discriminantValue;
}
和用法:
type A = { type: "A" };
type B = { type: "B" };
type C = { type: "C" };
type Union = A | B | C;
const arr: Union[] = [];
arr.find(isDiscriminate("type", "A")); // A
arr.find(isDiscriminate("type", ["A", "B"])); // A | B
arr.find(isNotDiscriminate("type", "A")); // B | C
arr.find(isNotDiscriminate("type", ["A", "B"])) // C
接受的答案很好,但很难理解。我做了这样的事情。它的可重用性较低,仅适用于一种类型,但更易于阅读。
function isType<V extends Union['type']>(val: V) {
return (obj: Union):
obj is Extract<Union, {type: V}> => obj.type === val;
}
const found = arr.find(isType('Foo'));
感谢 jcalz
我为 predicate guards 注释了一种稍微不同的工厂风格:
function hasProp<K extends PropertyKey, V extends string | number | boolean>(k: K, v: V) {
// All candidate types might have key `K` of any type
type Candidate = Partial<Record<K, any>> | null | undefined
// All matching subtypes of T must have key `K` equal value `V`
type Match<T extends Candidate> = Extract<T, Record<K, V>>
return <T extends Candidate>(obj: T): obj is Match<T> => (
obj?.[k] === v
)
}