【问题标题】:Extending Union Types Alias in TypeScript?在 TypeScript 中扩展联合类型别名?
【发布时间】:2021-08-05 16:23:34
【问题描述】:

我试图在编译时将某些字符串字段限制为仅具有某些值。问题是这些值应该是可扩展的。 这是一个简化的示例:

type Foobar = 'FOO' | 'BAR';

interface SomeInterface<T extends Foobar> {
  amember: T;

  [key: string]: string; // this really has to stay
}

// let's test it

const yes = {
    amember: 'FOO'
} as SomeInterface<'FOO'>; // compiles as expected

//    const no = {
//        amember: 'BAZ'
//    } as SomeInterface<'BAZ'>; // Type '"BAZ"' does not satisfy the constraint 'Foobar' as expected

// so far so good
// Now the problem

abstract class SomeClass<T extends Foobar> {
  private anotherMember: SomeInterface<T>;
}

type Foobarbaz = Foobar | 'BAZ';

class FinalClass extends SomeClass<Foobarbaz> { //no good anymore
}

错误是

类型“Foobarbaz”不满足约束“Foobar”。类型 '"BAZ"' 不能分配给类型 'Foobar'。

所以问题是:如何在打字稿中将“类型”限制为仅属于某些字符串,但是否可以使用其他字符串进行扩展? 还是这是一个 XY 问题并且有明显更好的解决方案?

Typescript 2.3.4 但我认为如果那里有魔法我可以升级到 2.4。

【问题讨论】:

    标签: typescript alias


    【解决方案1】:

    我认为您使用“可扩展”一词的含义与关键字 extends 的含义不同。通过说类型是“可扩展的”,您是说您希望能够加宽类型以接受更多值。但是当extends 某个类型时,这意味着您正在缩小 类型以接受更少 值。

    SomeInterface&lt;T extends Foobar&gt;本质上只能是以下四种类型之一:

    • SomeInterface&lt;'FOO'|'BAR'&gt;: amember 可以是'FOO''BAR'
    • SomeInterface&lt;'FOO'&gt;:amember只能是'FOO'
    • SomeInterface&lt;'BAR'&gt;:amember只能是'BAR'
    • SomeInterface&lt;never&gt;: amember 不能取任何值

    我有点怀疑这是否真的是你想要的,但只有你自己知道。


    另一方面,如果您希望将 SomeInterface&lt;T&gt; 定义为 T 可以始终FOOBAR,但也可能是其他一些 string 值,您需要 TypeScript 不完全提供的东西,即为 T 指定下限。 SomeInterface&lt;TsuperFoobar extends string&gt; 之类的东西不是有效的 TypeScript。

    但您可能只关心 amember 的类型,而不关心 T。如果您希望 amemberFOOBAR,但也可能是其他一些 string 值,您可以这样指定:

    interface SomeInterface<T extends string = never> {
      amember: Foobar | T;
      [key: string]: string; 
    }
    

    其中T 只是您希望允许的额外文字的联合。如果您不想允许任何额外内容,请使用never,或者直接省略类型参数(因为我已将never 作为默认值)。

    让我们看看它的实际效果:

    const yes = {
        amember: 'FOO'
    } as SomeInterface; // good, 'FOO' is a Foobar
    
    const no = {
        amember: 'BAZ'
    } as SomeInterface; // bad, 'BAZ' is not a Foobar
    
    abstract class SomeClass<T extends string> {
      private anotherMember: SomeInterface<T>;
    }
    
    class FinalClass extends SomeClass<'BAZ'> { 
    } // fine, we've added 'BAZ'
    
    // let's make sure we did:
    type JustChecking = FinalClass['anotherMember']['amember']
    // JustChecking === 'BAZ' | 'FOO' | 'BAR'
    

    我回答你的问题了吗?希望对您有所帮助。

    【讨论】:

    • 是的,这似乎可以解决问题。唯一纯粹的担忧是amember: Foobar | T; 读起来像 T 可以是任何东西,而不是 Foobar 的后代。但那是未成年人。我猜你是对的关于错过super
    【解决方案2】:

    要实现您的需要,您可以通过&amp; 符号使用intersection

    type Foobar = 'FOO' | 'BAR';
    type FoobarBaz = Foobar | & 'BAZ'; // or: 'BAZ' | & Foobar
    

    【讨论】:

    • Prettier 去掉了多余的 &amp;。你能链接这个技巧的来源吗?
    • @jayarjo 有一个指向(现已弃用)交叉点类型文档的链接。不过,我找不到任何最新的文档。
    • '&' 符号实际上是不必要的。剥离它会产生相同的类型。
    【解决方案3】:

    如何在 typescript 中将“类型”限制为仅属于某些字符串,但是否可以使用其他字符串进行扩展?

    不知道你的 X 问题是什么,但你总是可以引入另一个泛型参数,并通过声明它扩展该参数来限制类型。 Typescript 2.3 supports default types for generic parameters,因此默认使用Foobar,您可以像以前一样使用带有一个参数的SomeInterface,当您需要它来扩展其他内容时,您可以明确提供:

    type Foobar = 'FOO' | 'BAR';
    
    interface SomeInterface<T extends X, X extends string=Foobar> {
      amember: T;
    
      [key: string]: string; // this really has to stay
    }
    
    // let's test it
    
    const yes = {
        amember: 'FOO'
    } as SomeInterface<'FOO'>; // compiles as expected
    
    
    abstract class SomeClass<T extends X, X extends string=Foobar> {
      private anotherMember: SomeInterface<T, X>;
    }
    
    type Foobarbaz = Foobar | 'BAZ';
    
    class FinalClass extends SomeClass<Foobarbaz, Foobarbaz> { 
    }
    

    更新

    我想我现在明白了这个问题。一种解决方案是将联合类型(如 Foobar)编码为 keyof 的一些人工接口类型,仅用于表示键(不使用值类型并且无关紧要)。这样,通过扩展界面,您可以“自然”地扩展键集:

    interface FoobarKeys { FOO: { }; BAR: { } };
    
    type Foobar = keyof FoobarKeys;
    
    interface SomeInterface<X extends FoobarKeys = FoobarKeys> {
      amember: keyof X;
    
      [key: string]: string; // this really has to stay
    }
    
    
    
    abstract class SomeClass<X extends FoobarKeys = FoobarKeys> {
        protected anotherMember: SomeInterface<X> = {
            amember: 'FOO'
        };
    
        protected amethod(): void { 
            this.bmethod(this.anotherMember); // no error
        }
    
        protected bmethod(aparam: SomeInterface<X>): void { 
    
        }
    }
    
    // let's extend it
    
    interface FoobarbazKeys extends FoobarKeys { BAZ: {} };
    type Foobarbaz = keyof FoobarbazKeys;
    
    class FinalClass extends SomeClass<FoobarbazKeys> {
        private f() {
            this.bmethod({amember: 'BAZ'})
        } 
    }
    

    【讨论】:

    • 问题中的 X 是这样的:基类应该只能对任何 Foobar 进行操作,而子类可能具有附加值。这是扩展的:
    • 在 cmets 中格式化很难,因此链接 gist
    • 最终我选择了@jcalz 方式,这就是我将其标记为正确的原因。对于keyof 运算符,您的说法也是正确的和额外的荣誉。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-07-18
    • 2022-11-25
    • 2016-08-02
    • 1970-01-01
    • 2019-11-02
    • 1970-01-01
    • 2016-02-19
    相关资源
    最近更新 更多