强制Typescript类方法成为带有特定类型参数的一元方法

时间:2019-06-12 20:52:20

标签: typescript typescript-typings typescript-generics

为了好玩,我想看看是否可以提出一种仅使用类型的方法,该方法将迫使类中的所有函数成为一元函数,只要该对象符合指定的接口,它就可以接受任何对象(在这种情况下,我想确保提供了ContextObj作为参数)

我像这样刺了一下

interface BaseContext {}
interface SomeContext { foo: 'lish'; };
interface ContextObj<T> { context: T }

// a unary function that takes an argument that conforms to the ContextObj interface
// and returns a value of type U
type ContextFn<T, U> = (x: ContextObj<T>)  => U;

// a type to represent any function
type AnyFunction = (...args: any[]) => any;

// we use mapped types and conditional types here to determine if
// T[K], the value of the property K on type T, is a function
// if it's not a function we return the T[K] unaltered
// if it is a function we make sure it conforms to our ContextFn type
// otherwise we return never which I was hoping would result in an error
type ContextService<T, U extends BaseContext> = {
  [K in keyof T]: T[K] extends AnyFunction
  ? T[K] extends ContextFn<U, ReturnType<T[K]>>
    ? T[K]
    : never
  : T[K]
};

class BarService implements ContextService<BarService, SomeContext> {
  test:string = 'test';
  // expected error: not assignable to type never
  updateBar(): string {
    return '';
  }
}

事实证明,此操作不符合预期,因为它是TypeScript的限制(或功能?):https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-with-fewer-parameters-assignable-to-functions-that-take-more-parameters

我很好奇,是否有一种方法可以在类或接口级别上执行类似的操作(强制方法参数匹配特定的类型签名)?是否可以通过装饰器在单个方法级别完成操作(在此点上,您还可以只使用适当的类型签名)。

只需测试一下限制:)

1 个答案:

答案 0 :(得分:1)

您只需要为参数的长度添加另一个条件。您可以使用Parameters将参数的类型提取为元组。元组将具有数字文字类型的length属性,因此您可以检查length是否扩展了1

type ContextService<T, U extends BaseContext> = {
    [K in keyof T]: T[K] extends AnyFunction ?
        T[K] extends ContextFn<U, ReturnType<T[K]>> ?
            Parameters<T[K]>["length"] extends 1 ? T[K]
        : never : never
    : T[K]
};

class BarService implements ContextService<BarService, SomeContext> {
    test: string = 'test';
    // error: not assignable to type never
    updateBar(): string {
        return '';
    }
    updateBar2(p: ContextObj<SomeContext>): string { // ok
        return '';
    }
}

对于提示用户该问题可能很有用,尽管不支持自定义错误,但这非常接近:

type ContextService<T, U extends BaseContext> = {
    [K in keyof T]: T[K] extends AnyFunction ?
        T[K] extends ContextFn<U, ReturnType<T[K]>> ?
            Parameters<T[K]>["length"] extends 1 ? T[K]
        : ["Method must have exactly one parameter of type ", ContextObj<U>, "Found Parameters:", Parameters<T[K]>] 
        : ["Parameters types not macthed, expected  [", ContextObj<U>, "] Found Parameters:", Parameters<T[K]>]
    : T[K]
};

class BarService implements ContextService<BarService, SomeContext> {
    test: string = 'test';
    // error: not assignable to type never
    updateBar(): string {// Type '() => string' is missing the following properties from type '["Method must have exactly one parameter of type ", ContextObj<SomeContext>, "Found Parameters:", []]'
        return '';
    }
    updateBar2(p: ContextObj<SomeContext>): string {
        return '';
    }

    updateBar3(p: SomeContext): string { // Type '(p: SomeContext) => string' is missing the following properties from type '["Parameters types not macthed, expected  [", ContextObj<SomeContext>, "] Found Parameters:", [SomeContext]]'
        return '';
    }
}

修改

回答评论问题:这可以用装饰器完成吗?

答案是肯定的,我们可以捕获装饰器所应用的类以及它所应用的键作为类型参数,并使用与之前对属性键相同的逻辑。只是这次我们将错误附加到传递给装饰器的键上:

type Check<TSig extends (...a: any[]) => any, T, K extends keyof T> = 
    T[K] extends (...a: any[]) => any ?
    T[K] extends TSig ?
            Parameters<T[K]>["length"] extends 1 ? unknown
        : ["Method must have exactly one parameter of type ", Parameters<TSig>, "Found Parameters:", Parameters<T[K]>] 
        : ["Parameters types not macthed, expected  [", Parameters<TSig>, "] Found Parameters:", Parameters<T[K]>]
    : unknown
function ensureSignatire<TSig extends (...a: any[]) => any>() {
    return function <TTarget, TKey extends keyof TTarget>(target: TTarget, key: TKey & Check<TSig, TTarget, TKey>) {

    }
}

class BarService {
    test: string = 'test';

    @ensureSignatire<ContextFn<SomeContext, any>>() // Type '"updateBar"' is not assignable to type '["Method must have exactly one parameter of type ", [ContextObj<SomeContext>], "Found Parameters:", []]'.
    updateBar(): string {
        return '';
    }

    @ensureSignatire<ContextFn<SomeContext, any>>() //ok
    updateBar2(p: ContextObj<SomeContext>): string {
        return '';
    }
    @ensureSignatire<ContextFn<SomeContext, any>>() //  Type '"updateBar3"' is not assignable to type '["Parameters types not macthed, expected  [", [ContextObj<SomeContext>], "] Found Parameters:", [SomeContext]]'
    updateBar3(p: SomeContext): string { 
        return '';
    }
}