TypeScript 无法确定类型,即使它具有所有必要的信息
TypeScript does not determine the type even if it has all necessary information
我在 TypeScript 类型中遇到了这个问题。我在同一界面中使用基于不同 属性 的条件类型来确定一个 属性 的类型。这里,FruitBasket
中的 属性 slicer
是 AppleSlicer
或 BananaSlicer
取决于 type
.
如果您查看函数 test
,它接收 FruitBasket
的实例,可以是 Apple
或 Banana
。因此,我通过检查 type
是否等于其中之一来缩小类型,但它仍然抱怨 basket.slicer
不是确定性的。但是,它应该具有确定它是 AppleSlicer
所需的所有信息。我该如何解决这个问题?
我正在使用 TypeScript 4.5.4。
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
// These interfaces can have completely different shapes as shown.
interface AppleSlicer { apple(): void }
interface BananaSlicer { banana(): void }
export type FruitSlicer<TType> = TType extends typeof Fruits.Apple
? AppleSlicer
: TType extends typeof Fruits.Banana
? BananaSlicer
: never
export interface FruitBasket<TType extends Fruits> {
type: TType
slicer: FruitSlicer<TType>
}
const test = (basket: FruitBasket<Fruits>) => {
if (basket.type === Fruits.Apple) {
// This gives me a compile error because basket.slicer is either AppleSlicer or BananaSlicer.
// But, it should have all information it needs to deduce that it can only be an AppleSlicer
basket.slicer.apple()
}
}
我的首选模式是使切片器适应通用模式,这样您就不必进行此类检查。 (我会在几分钟内更新)不过,你的问题的答案是有一个明确的类型保护来正确检查类型,这将 return 向类型系统确认它确实符合你的条件是特定类型。
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
// These interfaces can have completely different shapes as shown.
interface AppleSlicer { apple(): void }
interface BananaSlicer { banana(): void }
export type FruitSlicer<TType> = TType extends typeof Fruits.Apple
? AppleSlicer
: TType extends typeof Fruits.Banana
? BananaSlicer
: never
export interface FruitBasket<TType extends Fruits> {
type: TType
slicer: FruitSlicer<TType>
}
const test = (basket: FruitBasket<Fruits>) => {
if (isAppleBasket(basket)) {
// This gives me a compile error because basket.slicer is either AppleSlicer or BananaSlicer.
// But, it should have all information it needs to deduce that it can only be an AppleSlicer
basket.slicer.apple()
}
else if (isBananaBasket(basket)) {
basket.slicer.banana()
}
}
function isAppleBasket(candidate: FruitBasket<Fruits>): candidate is FruitBasket<Fruits.Apple> {
return candidate.type === Fruits.Apple;
}
function isBananaBasket(candidate: FruitBasket<Fruits>): candidate is FruitBasket<Fruits.Banana> {
return candidate.type === Fruits.Banana;
}
更新
所以事实证明,转换现有代码并不像我想象的那么容易,因为从枚举扩展与 class 或接口的工作方式不同。这是我更新的例子,有一些解释。如果您想使用 classes 或带有类型的普通 javascript 对象,则由您决定。如果只是为了能够使用 instanceof
.
,我更喜欢 classes
这不一定会向您展示我提到的适配器模式(将您的 interface/class 与另一个包装在一起以使它们具有相同的功能集),但它显示了如果您这样做会发生什么(FruitSlicer<T extends Fruit>
)
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
interface Fruit {
fruitType: Fruits;
}
class Apple implements Fruit {
public fruitType: Fruits = Fruits.Apple;
}
class Banana implements Fruit {
public fruitType: Fruits = Fruits.Banana;
}
interface SlicedFruit {
fruitType: Fruits;
numberOfSlices: number;
}
interface FruitSlicer<T extends Fruit> {
fruitType: Fruits;
canHandleFruit(fruit: Fruit): boolean;
slice(fruit: T): SlicedFruit;
}
class AppleSlicer implements FruitSlicer<Apple> {
public fruitType = Fruits.Apple;
public canHandleFruit(fruit: Fruit): boolean {
return fruit.fruitType === this.fruitType;
}
public slice(fruit: Apple): SlicedFruit{
return {
// if we turn the number of slices into a property,
// we could make an abstract base class that does this work for us
// for any future fruit.
fruitType: fruit.fruitType,
numberOfSlices: 8
}
}
}
class BananaSlicer implements FruitSlicer<Banana> {
public fruitType = Fruits.Banana;
public canHandleFruit(fruit: Fruit): boolean {
return fruit.fruitType === this.fruitType;
}
public slice(fruit: Banana): SlicedFruit {
return {
fruitType: Fruits.Banana,
numberOfSlices: 15
}
}
}
const allSlicers: FruitSlicer<Fruit>[] = [ new AppleSlicer(), new BananaSlicer() ]
// in terms of objects, a basket can hold several types of fruit. We could make a basket of
// all bananas or all apples, but it doesn't make sense to have ONLY that option.
// this example has a full basket of any fruit type, and we decide for each fruit
// what type of slicer to use later on.
const basket: Fruit[] = [ new Apple(), new Banana(), new Banana(), new Apple()];
basket.forEach(f => {
// allSlicers.find could make use of the factory pattern,
// and could be more performant with a map lookup, etc.
const slicedResult: SlicedFruit | undefined = allSlicers.find(s => s.canHandleFruit(f))?.slice(f);
console.log(!slicedResult ? 'null' : `${slicedResult?.fruitType} sliced ${ slicedResult.numberOfSlices } ways!`);
})
您可以编写一个type predicates函数来在使用篮子之前检查篮子的类型。
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
// These interfaces can have completely different shapes as shown.
interface AppleSlicer { apple(): void }
interface BananaSlicer { banana(): void }
export type FruitSlicer<TType> = TType extends typeof Fruits.Apple
? AppleSlicer
: TType extends typeof Fruits.Banana
? BananaSlicer
: never
export interface FruitBasket<TType extends Fruits> {
type: TType
slicer: FruitSlicer<TType>
}
function checkTypeOfBasket<T extends Fruits>(basket: FruitBasket<Fruits>, type: T): basket is FruitBasket<T> {
return basket.type === type
}
const test = (basket: FruitBasket<Fruits>) => {
if (checkTypeOfBasket(basket, Fruits.Apple)) {
// after checking, typescript understands the basket is an apple basket
basket.slicer.apple()
}
}
这里的大问题是 FruitBasket<Fruits>
根本不是一个合适的 discriminated union where you can check a discriminant property (which would be type
) to narrow the type of the object. Not only isn't it a discriminated union, it's not even a union。相当于
{ type: Fruits, slicer: AppleSlicer | BananaSlicer }
因此,根据该定义,这是一个完全有效的 FruitBasket<Fruits>
:
const whoops: FruitBasket<Fruits> = {
type: Fruits.Apple,
slicer: { banana() { } }
} // okay
仅仅因为 type
是 Fruits.Apple
,并不意味着 slicer
将是 FruitSlicer<Fruits.Apple>
。所以如果 test()
接受 FruitBasket<Fruits>
,那么它接受 whoops
:
test(whoops) // no error here either
和那个意味着test()
的实现真的不能通过查看basket.type
安全地得出任何关于basket.slicer
的结论。编译器错误是有效的。哎呀
所以你不希望 test
接受 FruitBasket<Fruits>
。你想要的是联合类型 FruitBasket<Fruits.Apple> | FruitBasket<Fruts.Banana>
,一个判别式联合,其中 type
是判别式 属性.
如果您不想手动写出该类型(例如,您的 enum
中还有很多其他 Fruits
),您可以从您的 [= 版本生成此联合36=]如下:
type FruitBasketUnion = { [F in Fruits]: FruitBasket<F> }[Fruits]
// type FruitBasketUnion = FruitBasket<Fruits.Apple> | FruitBasket<Fruits.Banana>
我们在这里创建一个 mapped type with a FruitBasket<F>
property type for each F
in Fruits
, and then immediately indexing into 映射类型 Fruits
以获得所需的联合。
现在我们可以将 test()
的参数类型设为 basket.type === Fruits.Apple
确实 将 basket
的类型缩小为FruitBasket<Fruits.Apple>
:
const test = (basket: FruitBasketUnion) => {
if (basket.type === Fruits.Apple) {
basket.slicer.apple() // okay
}
}
现在 test()
实现编译没有错误。那最好意味着你不能再打电话给 test(whoops)
:
test(whoops); // error!
// ~~~~~~
// Argument of type 'FruitBasket<Fruits>' is not assignable to
// parameter of type 'FruitBasketUnion'.
所以编译器正确地拒绝了 whoops
。让我们确保它接受 FruitBasket<Fruits.Apple>
和 FruitBasket<Fruits.Banana>
:
test({
type: Fruits.Banana,
slicer: { banana() { } }
}); // okay
test({
type: Fruits.Apple,
slicer: { apple() { } }
}); // okay
看起来不错。
我在 TypeScript 类型中遇到了这个问题。我在同一界面中使用基于不同 属性 的条件类型来确定一个 属性 的类型。这里,FruitBasket
中的 属性 slicer
是 AppleSlicer
或 BananaSlicer
取决于 type
.
如果您查看函数 test
,它接收 FruitBasket
的实例,可以是 Apple
或 Banana
。因此,我通过检查 type
是否等于其中之一来缩小类型,但它仍然抱怨 basket.slicer
不是确定性的。但是,它应该具有确定它是 AppleSlicer
所需的所有信息。我该如何解决这个问题?
我正在使用 TypeScript 4.5.4。
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
// These interfaces can have completely different shapes as shown.
interface AppleSlicer { apple(): void }
interface BananaSlicer { banana(): void }
export type FruitSlicer<TType> = TType extends typeof Fruits.Apple
? AppleSlicer
: TType extends typeof Fruits.Banana
? BananaSlicer
: never
export interface FruitBasket<TType extends Fruits> {
type: TType
slicer: FruitSlicer<TType>
}
const test = (basket: FruitBasket<Fruits>) => {
if (basket.type === Fruits.Apple) {
// This gives me a compile error because basket.slicer is either AppleSlicer or BananaSlicer.
// But, it should have all information it needs to deduce that it can only be an AppleSlicer
basket.slicer.apple()
}
}
我的首选模式是使切片器适应通用模式,这样您就不必进行此类检查。 (我会在几分钟内更新)不过,你的问题的答案是有一个明确的类型保护来正确检查类型,这将 return 向类型系统确认它确实符合你的条件是特定类型。
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
// These interfaces can have completely different shapes as shown.
interface AppleSlicer { apple(): void }
interface BananaSlicer { banana(): void }
export type FruitSlicer<TType> = TType extends typeof Fruits.Apple
? AppleSlicer
: TType extends typeof Fruits.Banana
? BananaSlicer
: never
export interface FruitBasket<TType extends Fruits> {
type: TType
slicer: FruitSlicer<TType>
}
const test = (basket: FruitBasket<Fruits>) => {
if (isAppleBasket(basket)) {
// This gives me a compile error because basket.slicer is either AppleSlicer or BananaSlicer.
// But, it should have all information it needs to deduce that it can only be an AppleSlicer
basket.slicer.apple()
}
else if (isBananaBasket(basket)) {
basket.slicer.banana()
}
}
function isAppleBasket(candidate: FruitBasket<Fruits>): candidate is FruitBasket<Fruits.Apple> {
return candidate.type === Fruits.Apple;
}
function isBananaBasket(candidate: FruitBasket<Fruits>): candidate is FruitBasket<Fruits.Banana> {
return candidate.type === Fruits.Banana;
}
更新
所以事实证明,转换现有代码并不像我想象的那么容易,因为从枚举扩展与 class 或接口的工作方式不同。这是我更新的例子,有一些解释。如果您想使用 classes 或带有类型的普通 javascript 对象,则由您决定。如果只是为了能够使用 instanceof
.
这不一定会向您展示我提到的适配器模式(将您的 interface/class 与另一个包装在一起以使它们具有相同的功能集),但它显示了如果您这样做会发生什么(FruitSlicer<T extends Fruit>
)
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
interface Fruit {
fruitType: Fruits;
}
class Apple implements Fruit {
public fruitType: Fruits = Fruits.Apple;
}
class Banana implements Fruit {
public fruitType: Fruits = Fruits.Banana;
}
interface SlicedFruit {
fruitType: Fruits;
numberOfSlices: number;
}
interface FruitSlicer<T extends Fruit> {
fruitType: Fruits;
canHandleFruit(fruit: Fruit): boolean;
slice(fruit: T): SlicedFruit;
}
class AppleSlicer implements FruitSlicer<Apple> {
public fruitType = Fruits.Apple;
public canHandleFruit(fruit: Fruit): boolean {
return fruit.fruitType === this.fruitType;
}
public slice(fruit: Apple): SlicedFruit{
return {
// if we turn the number of slices into a property,
// we could make an abstract base class that does this work for us
// for any future fruit.
fruitType: fruit.fruitType,
numberOfSlices: 8
}
}
}
class BananaSlicer implements FruitSlicer<Banana> {
public fruitType = Fruits.Banana;
public canHandleFruit(fruit: Fruit): boolean {
return fruit.fruitType === this.fruitType;
}
public slice(fruit: Banana): SlicedFruit {
return {
fruitType: Fruits.Banana,
numberOfSlices: 15
}
}
}
const allSlicers: FruitSlicer<Fruit>[] = [ new AppleSlicer(), new BananaSlicer() ]
// in terms of objects, a basket can hold several types of fruit. We could make a basket of
// all bananas or all apples, but it doesn't make sense to have ONLY that option.
// this example has a full basket of any fruit type, and we decide for each fruit
// what type of slicer to use later on.
const basket: Fruit[] = [ new Apple(), new Banana(), new Banana(), new Apple()];
basket.forEach(f => {
// allSlicers.find could make use of the factory pattern,
// and could be more performant with a map lookup, etc.
const slicedResult: SlicedFruit | undefined = allSlicers.find(s => s.canHandleFruit(f))?.slice(f);
console.log(!slicedResult ? 'null' : `${slicedResult?.fruitType} sliced ${ slicedResult.numberOfSlices } ways!`);
})
您可以编写一个type predicates函数来在使用篮子之前检查篮子的类型。
enum Fruits {
Apple = "Apple",
Banana = "Banana",
}
// These interfaces can have completely different shapes as shown.
interface AppleSlicer { apple(): void }
interface BananaSlicer { banana(): void }
export type FruitSlicer<TType> = TType extends typeof Fruits.Apple
? AppleSlicer
: TType extends typeof Fruits.Banana
? BananaSlicer
: never
export interface FruitBasket<TType extends Fruits> {
type: TType
slicer: FruitSlicer<TType>
}
function checkTypeOfBasket<T extends Fruits>(basket: FruitBasket<Fruits>, type: T): basket is FruitBasket<T> {
return basket.type === type
}
const test = (basket: FruitBasket<Fruits>) => {
if (checkTypeOfBasket(basket, Fruits.Apple)) {
// after checking, typescript understands the basket is an apple basket
basket.slicer.apple()
}
}
这里的大问题是 FruitBasket<Fruits>
根本不是一个合适的 discriminated union where you can check a discriminant property (which would be type
) to narrow the type of the object. Not only isn't it a discriminated union, it's not even a union。相当于
{ type: Fruits, slicer: AppleSlicer | BananaSlicer }
因此,根据该定义,这是一个完全有效的 FruitBasket<Fruits>
:
const whoops: FruitBasket<Fruits> = {
type: Fruits.Apple,
slicer: { banana() { } }
} // okay
仅仅因为 type
是 Fruits.Apple
,并不意味着 slicer
将是 FruitSlicer<Fruits.Apple>
。所以如果 test()
接受 FruitBasket<Fruits>
,那么它接受 whoops
:
test(whoops) // no error here either
和那个意味着test()
的实现真的不能通过查看basket.type
安全地得出任何关于basket.slicer
的结论。编译器错误是有效的。哎呀
所以你不希望 test
接受 FruitBasket<Fruits>
。你想要的是联合类型 FruitBasket<Fruits.Apple> | FruitBasket<Fruts.Banana>
,一个判别式联合,其中 type
是判别式 属性.
如果您不想手动写出该类型(例如,您的 enum
中还有很多其他 Fruits
),您可以从您的 [= 版本生成此联合36=]如下:
type FruitBasketUnion = { [F in Fruits]: FruitBasket<F> }[Fruits]
// type FruitBasketUnion = FruitBasket<Fruits.Apple> | FruitBasket<Fruits.Banana>
我们在这里创建一个 mapped type with a FruitBasket<F>
property type for each F
in Fruits
, and then immediately indexing into 映射类型 Fruits
以获得所需的联合。
现在我们可以将 test()
的参数类型设为 basket.type === Fruits.Apple
确实 将 basket
的类型缩小为FruitBasket<Fruits.Apple>
:
const test = (basket: FruitBasketUnion) => {
if (basket.type === Fruits.Apple) {
basket.slicer.apple() // okay
}
}
现在 test()
实现编译没有错误。那最好意味着你不能再打电话给 test(whoops)
:
test(whoops); // error!
// ~~~~~~
// Argument of type 'FruitBasket<Fruits>' is not assignable to
// parameter of type 'FruitBasketUnion'.
所以编译器正确地拒绝了 whoops
。让我们确保它接受 FruitBasket<Fruits.Apple>
和 FruitBasket<Fruits.Banana>
:
test({
type: Fruits.Banana,
slicer: { banana() { } }
}); // okay
test({
type: Fruits.Apple,
slicer: { apple() { } }
}); // okay
看起来不错。