заставить метод класса машинописи быть унарным методом с аргументом определенного типа - PullRequest
1 голос
/ 12 июня 2019

Ради интереса я хотел посмотреть, смогу ли я придумать способ, использующий только типы, который заставил бы все функции в классе быть унарной функцией, которая принимает любой объект, если этот объект соответствует указанному интерфейсу ( в этом случае я хотел убедиться, что в качестве аргумента было указано 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 Ответ

1 голос
/ 13 июня 2019

Вам просто нужно добавить еще одно условие для длины параметров. Вы можете извлечь тип параметров в виде кортежа, используя Parameters. Кортеж будет иметь свойство length с числовым литералом, поэтому вы можете проверить, что length extends 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 '';
    }
}
...