在 TypeScript 中扩展联合类型别名?

Extending Union Types Alias in TypeScript?

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

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
}

错误是

Type 'Foobarbaz' does not satisfy the constraint 'Foobar'. Type '"BAZ"' is not assignable to type 'Foobar'.

所以问题是:如何在 typescript 中将 'type' 限制为仅包含某些字符串,但可以将其扩展为其他字符串? 或者这是一个 XY 问题并且有明显更好的解决方案?

Typescript 2.3.4,但我想我可以升级到 2.4,如果那里有魔法的话。

how in typescript can I limit a 'type' to be of certain strings only, but have it extendable with other strings?

不知道你的 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,仅用于表示键(不使用值类型,也无关紧要)。这样,通过扩展接口,您可以 "naturally" 扩展键集:

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'})
    } 
}

我认为您使用 "extendable" 一词的意义与关键字 extends 的意义不同。通过说类型是 "extendable" 你是说你希望能够 扩大 类型以接受 更多 值。但是当某些东西 extends 是一种类型时,这意味着您正在 缩小 类型以接受 更少的 值。

SomeInterface<T extends Foobar>本质上只能是以下四种类型之一:

  • SomeInterface<'FOO'|'BAR'>: amember 可以是 'FOO''BAR'
  • SomeInterface<'FOO'>: amember只能是'FOO'
  • SomeInterface<'BAR'>: amember只能是'BAR'
  • SomeInterface<never>: amember 不能取任何值

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


另一方面,如果您希望 SomeInterface<T> 被定义为 T 可以 始终FOOBAR,但也可能是其他一些 string 值,您需要 TypeScript 不完全提供的东西,这将指定 T 的下限。像 SomeInterface<TsuperFoobar extends string> 这样的东西不是有效的 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'

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

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

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