如何在使用通用参数作为属性名称的打字稿中声明通用接口?

时间:2019-02-13 09:43:27

标签: typescript generics

我想声明一些这样的接口:

interface Foo<K extends string> {
    [k in K]: string // TS error: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
}

用法:

let b: Foo<'bar'> = { bar: '123' }
let c: Foo<'a' | 'b'> = { a: '123', b: '321' }

有可能吗?


更新:

我不能只使用Record实用程序泛型或类型别名,因为我实际上想使用polymorphic this type

interface Foo<K extends string> {
    [k in K] (): this // 
}

class Bar implements Foo<'sorted' | 'reversed'> { /* omitted */ }

let foo: Bar
foo.sorted() // returns Bar
foo.reversed().sorted().sorted().reversed() // still Bar

2 个答案:

答案 0 :(得分:2)

接口不能包含映射类型,只有类型别名可以:

type Foo<K extends string> = {
    [k in K]: string 
}


let b: Foo<'bar'> = { bar: '123' }
let c: Foo<'a' | 'b'> = { a: '123', b: '321' }

请注意,此类型与Record非常相似,您可能只想使用它。这等效于显式映射类型:

type Foo<K extends string> = Record<K, string>

答案 1 :(得分:0)

@Titian Cernicova-Dragomir的回答对于原始问题是正确的,而我的实际潜在问题要比问题本身复杂得多。我将解决方案发布在这里,以防其他人有类似需求到达这里。

实际问题是: 假设我有一个类TClass和一个函数addMethod,该函数向TClass.prototype添加了一些方法。换句话说,addMethod充当装饰器,但是我省略了@装饰器语法,因为它至少在目前不能更改要装饰的类的签名(请参见跟踪的Github issue此功能)。

如果addMethod仅被调用一次,则通过编写递归类型别名可以轻松解决该问题:

type Ctor<T, Args extends any[] = any[]> = new (...args: Args) => T
type CtorParameters<T extends Ctor<any>> = T extends Ctor<any, infer P> ? P : never

type Augmented<T, K extends string, Args extends any[]> = T & { [k in K]: (...args: Args) => Augmented<T, K, Args> }

function addMethod<
    T extends Ctor<any>,
    K extends string,
    Args extends any[]
> (ctor: T, k: K, method: (this: InstanceType<T>, ...args: Args) => void): Ctor<Augmented<InstanceType<T>, K, Args>, CtorParameters<T>> {
    ctor.prototype[k] = function (...args: any[]) {
        method.apply(this, args)
        return this
    }
    return ctor
}

class Foo {
    constructor(public foo: number) {}
}

const Bar = addMethod(Foo, 'inc', function (delta: number)  {
    this.foo += delta
})

const bar = new Bar(1)
bar.inc(2)
console.log(bar.foo) // 3
bar.inc(3).inc(4)
console.log(bar.foo) // 10

但是,如果随后addMethod被多次调用,则结果构造函数的实例将受到某种程度的损害。添加的方法只有按特定顺序调用才能完全链接。


const Baz = addMethod(Bar, 'mult', function (factor: number) {
    this.foo *= factor
})

const baz = new Baz(2)

baz.mult(3).inc(4)
console.log(baz.foo) // 10, works

baz.inc(3).mult(4) // Property 'mult' does not exist on type 'Augmented<Foo, "inc", [number]>'.

这就是为什么我发布原始问题。我意识到,使用递归类型别名来表示扩充类型是行不通的,因为先前添加的方法的返回类型不包括后来添加的方法。然后,我认为将返回类型声明为this会有所帮助。但是this仅在classinterface内部受支持。因此是问题。

但是,我也意识到addMethod不需要多次调用,只要我可以在一次调用中提供方法即可。


type Augmented<T, Methods extends Record<string, (...args: any[]) => void>> = 
    T & { [k in keyof Methods]: (...args: Parameters<Methods[k]>) => Augmented<T, Methods> }

function addMethod<
    T extends Ctor<any>,
    Methods extends Record<string, (...args: any[]) => void>
> (ctor: T, methods: Methods & ThisType<InstanceType<T>>): Ctor<Augmented<InstanceType<T>, Methods>> {
    const keys = Object.keys(methods) as (keyof Methods)[]
    keys.forEach(k => {
        ctor.prototype[k] = function (...args: any[]) {
            methods[k].apply(this, args)
            return this
        }
    })
    return ctor
}


const Baz = addMethod(Foo, {
    inc: function (delta: number) {
        this.foo += delta
    },
    mult: function (factor: number) {
        this.foo *= factor
    }
})

const baz = new Baz(2)

baz.mult(3).inc(4)
console.log(baz.foo) // 10, works

baz.foo = 2
baz.inc(3).mult(4) 
console.log(baz.foo) // 20, works