Programing

새로 생성되지 않은 TypeScript 클래스의 생성자 호출

c10106 2022. 4. 5. 21:38
반응형

새로 생성되지 않은 TypeScript 클래스의 생성자 호출

에서는 자바스크립트의 할 수 할 수 있다.new:

function MyClass(val) {
    if (!(this instanceof MyClass)) {
        return new MyClass(val);
    }

    this.val = val;
}

그런 다음 건설할 수 있다.MyClass다음 문장 중 하나를 사용하는 개체:

var a = new MyClass(5);
var b = MyClass(5);

아래의 TypeScript 클래스를 사용하여 비슷한 결과를 얻으려고 노력했다.

class MyClass {
    val: number;

    constructor(val: number) {
        if (!(this instanceof MyClass)) {
            return new MyClass(val);
        }

        this.val = val;
    }
}

하지만 부름MyClass(5)나에게 오류를 준다.Value of type 'typeof MyClass' is not callable. Did you mean to include 'new'?

TypeScript에서 이 패턴이 작동하도록 할 수 있는 방법이 없을까?

이건 어때?의 원하는 모양을 설명하십시오.MyClass그리고 그 생성자:

interface MyClass {
  val: number;
}

interface MyClassConstructor {
  new(val: number): MyClass;  // newable
  (val: number): MyClass; // callable
}

에 유의하십시오.MyClassConstructor함수로서 호출 가능한 동시에 생성자로서 새로워질 수 있는 것으로 정의된다.그런 다음 이를 구현하십시오.

const MyClass: MyClassConstructor = function(this: MyClass | void, val: number) {
  if (!(this instanceof MyClass)) {
    return new MyClass(val);
  } else {
    this!.val = val;
  }
} as MyClassConstructor;

위 내용은 비록 작은 주름이 몇 개 있기는 하지만 효과가 있다. 첫째 returns문 문문 returns환MyClass | undefined그리고 컴파일러는 그 사실을 깨닫지 못한다.MyClass반환 값은 호출 가능한 함수와undefined값은 새로운 생성자에 해당함...그래서 불평을 늘어놓는다.그래서 더as MyClassConstructor맨 마지막에주름2: 더this매개변수가 현재 제어 흐름 분석을 통해 좁혀지지 않기 때문에 다음과 같이 주장해야 한다.this아니다void을 할 때val비록 그 시점에서 우리는 그것이 있을 수 없다는 것을 알고 있지만void그래서 우리는 null이 아닌 어설션 오퍼레이터를 사용해야 한다.

어쨌든 다음과 같은 작업이 가능한지 확인할 수 있다.

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

그게 도움이 되길 바래; 행운을 빌어!


갱신하다

주의사항: @Paleo의 대답에서 언급된 바와 같이, 만약 당신의 목표가 ES2015 이상이라면,class당신의 소스가 생산될 것이다.class컴파일된 JavaScript에서 필요한 것 new()사양대로나는 다음과 같은 오류를 보았다.TypeError: Class constructors cannot be invoked without 'new'일부 자바스크립트 엔진은 스펙을 무시하고 함수형 호출도 흔쾌히 받아들일 가능성이 있다.이러한 주의 사항(예: 타겟이 명시적으로 ES5이거나 이러한 비규격 환경 중 하나에서 실행될 것임을 알고 있는 경우)에 상관하지 않으면 TypeScript가 다음과 같이 수행되도록 할 수 있다.

class _MyClass {
  val: number;

  constructor(val: number) {
    if (!(this instanceof MyClass)) {
      return new MyClass(val);
    }

    this.val = val;
  }
}
type MyClass = _MyClass;
const MyClass = _MyClass as typeof _MyClass & ((val: number) => MyClass)

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

이 경우에는 이름을 바꾸셨습니다.MyClass에 방해가 되지 않는._MyClass, 및 정의됨MyClass둘 다 유형이다(과 동일)._MyClass 및 ) 및(동화)_MyClass구성자, 그러나 그 유형 또한 함수처럼 호출할 수 있다고 주장되는 유형)이것은 위에서 본 바와 같이 컴파일 시간에 작동한다.당신의 런타임이 만족스러운지 여부는 위의 주의사항의 적용을 받는다.개인적으로 기능 스타일은 es2015와 그 이후에서 호출 가능하고 새로운 기능이라는 것을 알기 때문에 원래 대답에서 고수하고 싶다.

다시 행운을 빈다!


업데이트 2

그냥 신고할 방법을 찾는 거라면.bindNew()이 답안으로부터 기능하는데, 그것은 스펙 트럼을 필요로 한다.class그리고 기능처럼 새로울 수도 있고 호출할 수도 있는 것을 생산하면 다음과 같은 것을 할 수 있다.

function bindNew<C extends { new(): T }, T>(Class: C & {new (): T}): C & (() => T);
function bindNew<C extends { new(a: A): T }, A, T>(Class: C & { new(a: A): T }): C & ((a: A) => T);
function bindNew<C extends { new(a: A, b: B): T }, A, B, T>(Class: C & { new(a: A, b: B): T }): C & ((a: A, b: B) => T);
function bindNew<C extends { new(a: A, b: B, d: D): T }, A, B, D, T>(Class: C & {new (a: A, b: B, d: D): T}): C & ((a: A, b: B, d: D) => T);
function bindNew(Class: any) {
  // your implementation goes here
}

이는 다음을 올바르게 입력하는 효과가 있다.

class _MyClass {
  val: number;

  constructor(val: number) {    
    this.val = val;
  }
}
type MyClass = _MyClass;
const MyClass = bindNew(_MyClass); 
// MyClass's type is inferred as typeof _MyClass & ((a: number)=> _MyClass)

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

하지만 에 대한 과중한 선언에 주의하라.bindNew()모든 경우를 위해 일하지 마십시오.특히 그것은 최대 3개의 필수 매개변수를 사용하는 건설업자에게 효과가 있다.선택적 매개 변수나 다중 과부하 서명이 있는 생성자는 아마도 적절하게 추론되지 않을 것이다.그래서 당신은 사용 사례에 따라 오타를 수정해야 할 수도 있다.

좋아, 그게 도움이 되길 바래.세 번째 행운을 빈다.


업데이트 3, 2018년 8월

TypeScript 3.0은 휴식과 스프레드 포지션에 튜플을 도입하여 위의 과부하와 제한 없이 임의의 수 및 유형의 논쟁 기능을 쉽게 처리할 수 있게 했다.여기 의 새로운 선언이 있다.bindNew():

declare function bindNew<C extends { new(...args: A): T }, A extends any[], T>(
  Class: C & { new(...args: A): T }
): C & ((...args: A) => T);

키드드newES6 클래스에 필요:

단, 함수 호출(규격의 9.2.2항)을 통해서만 클래스를 호출할 수 없다 [소스]

해결 방법instanceof그리고extends일하는

지금까지 사용했던 대부분의 솔루션의 문제x = X()대신에x = new X()다음과 같다:

  1. x instanceof X효과가 없다
  2. class Y extends X { }효과가 없다
  3. console.log(x)다른 활자를 인쇄하다X
  4. 추가적으로.x = X()그러나x = new X()하지 않다
  5. 최신 플랫폼(ES6)을 대상으로 할 때 전혀 작동하지 않는 경우가 있음

내 해결 방법

TL;DR - 기본 사용법

아래 코드(GitHub에도 있음 - ts-no-new 참조)를 사용하여 다음을 쓸 수 있다.

interface A {
  x: number;
  a(): number;
}
const A = nn(
  class A implements A {
    x: number;
    constructor() {
      this.x = 0;
    }
    a() {
      return this.x += 1;
    }
  }
);

또는:

class $A {
  x: number;
  constructor() {
    this.x = 10;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A = nn($A);

일반적인 방법 대신:

class A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
} 

둘 중 하나를 사용할 수 있다a = new A()또는a = A()작동하여instanceofextends, 현대 컴파일 대상에 대한 적절한 상속 및 지원(일부 솔루션은 ES5 이상에 의존하기 때문에 변환된 경우에만 작동함class로 번역된.function다른 호출 의미를 갖는 것.

전체 예시

#1

type cA = () => A;

function nonew<X extends Function>(c: X): AI {
  return (new Proxy(c, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as any as AI);
}

interface A {
  x: number;
  a(): number;
}

const A = nonew(
  class A implements A {
    x: number;
    constructor() {
      this.x = 0;
    }
    a() {
      return this.x += 1;
    }
  }
);

interface AI {
  new (): A;
  (): A;
}

const B = nonew(
  class B extends A {
    a() {
      return this.x += 2;
    }
  }
);

#2

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

class $A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A: MC<A> = nn($A);
Object.defineProperty(A, 'name', { value: 'A' });

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
const B: MC<B> = nn($B);
Object.defineProperty(B, 'name', { value: 'B' });

#3

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

type $c = { $c: Function };

class $A {
  static $c = A;
  x: number;
  constructor() {
    this.x = 10;
    Object.defineProperty(this, 'constructor', { value: (this.constructor as any as $c).$c || this.constructor });
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
var A: MC<A> = nn($A);
$A.$c = A;
Object.defineProperty(A, 'name', { value: 'A' });

class $B extends $A {
  static $c = B;
  a() {
    return this.x += 2;
  }
}
type B = $B;
var B: MC<B> = nn($B);
$B.$c = B;
Object.defineProperty(B, 'name', { value: 'B' });

2위 단순화

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

class $A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A: MC<A> = nn($A);

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
const B: MC<B> = nn($B);

3위 단순화

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

class $A {
  x: number;
  constructor() {
    this.x = 10;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
var A: MC<A> = nn($A);

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
var B: MC<B> = nn($B);

#1#2에서:

  • instanceof작동하다
  • extends작동하다
  • console.log올바르게 인쇄하다.
  • constructor .

#3:

  • instanceof작동하다
  • extends작동하다
  • console.log올바르게 인쇄하다.
  • constructor노출 포장지를 가리키는 인스턴스(상황에 따라 장단점이 될 수 있음) 특성

단순화된 버전은 필요 없는 경우 자기성찰을 위한 모든 메타데이터를 제공하지 않는다.

참고 항목

유형 및 함수에 대한 해결 방법:

class _Point {
    public readonly x: number;
    public readonly y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}
export type Point = _Point;
export function Point(x: number, y: number): Point {
    return new _Point(x, y);
}

또는 인터페이스 포함:

export interface Point {
    readonly x: number;
    readonly y: number;
}

class _PointImpl implements Point {
    public readonly x: number;
    public readonly y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

export function Point(x: number, y: number): Point {
    return new _PointImpl(x, y);
}

TL;DR

만약 당신이 ES6를 목표로 하고 있다면, 그리고 당신은 정말로 사용하기를 원한다.class데이터가 아닌 데이터를 저장하려면function:

  • 만들기function단순히 그 주장으로 당신의 클래스 생성자를 불러 일으킨다.
  • 그것을 설정하다.functionprototype에게prototype너희 반의

지금부터 당신은 그것을 부를 수 있다.function 있든 없든 new새 클래스 인스턴스를 생성하는 키워드.

활자놀이터


Typecript를 통해 이와 같은 내용을 만들 수 있는 기능 제공function("callable constructor"라고 부르자)는 강하게 타이핑을 했다.글쎄요.any형식은 중간 유형 정의에서 필요하다(그 정의와 함께 표시).unknown실수를 유발한다), 그러나 이 사실은 당신의 경험에 영향을 미치지 않을 것이다.

무엇보다도 먼저 우리가 함께 일하고 있는 실체를 기술하기 위한 기본 유형을 정의해야 한다.

// Let's assume "class X {}". X itself (it has type "typeof X") can be called with "new" keyword,
// thus "typeof X" extends this type
type Constructor = new(...args: Array<any>) => any;

// Extracts argument types from class constructor
type ConstructorArgs<TConstructor extends Constructor> =
    TConstructor extends new(...args: infer TArgs) => any ? TArgs : never;

// Extracts class instance type from class constructor
type ConstructorClass<TConstructor extends Constructor> =
    TConstructor extends new(...args: Array<any>) => infer TClass ? TClass : never;

// This is what we want: to be able to create new class instances
// either with or without "new" keyword
type CallableConstructor<TConstructor extends Constructor> =
  TConstructor & ((...args: ConstructorArgs<TConstructor>) => ConstructorClass<TConstructor>);

다음 단계는 정규 클래스 생성자를 받아들여 그에 상응하는 "콜러블 생성자"를 생성하는 함수를 작성하는 것이다.

function CreateCallableConstructor<TConstructor extends Constructor>(
    type: TConstructor
): CallableConstructor<TConstructor> {
    function createInstance(
        ...args: ConstructorArgs<TConstructor>
    ): ConstructorClass<TConstructor> {
        return new type(...args);
    }

    createInstance.prototype = type.prototype;
    return createInstance as CallableConstructor<TConstructor>;
}

이제 우리가 해야 할 일은 우리의 "콜러블 시공사"를 만들고 그것이 정말로 효과가 있는지 확인하는 것이다.

class TestClass {
  constructor(readonly property: number) { }
}

const CallableTestConstructor = CreateCallableConstructor(TestClass);

const viaCall = CallableTestConstructor(56) // inferred type is TestClass
console.log(viaCall instanceof TestClass) // true
console.log(viaCall.property) // 56

const viaNew = new CallableTestConstructor(123) // inferred type is TestClass
console.log(viaNew instanceof TestClass) // true
console.log(viaNew.property) // 123

CallableTestConstructor('wrong_arg'); // error
new CallableTestConstructor('wrong_arg'); // error

내가 이 문제를 해결한 방법은 다음과 같다.jest불변 모델 테스트 그룹용.makeHash함수는 특별한 일을 하는 것이 아니라 단지 유틸리티에서 짧고 무작위적인 문자열을 만드는 것이다.uuid().

내게는 '마법'이 선언하고 있었다.type로서new (...args: any[]) => any로서 그것을 '새로' 할 수 있도록 허락하다.let model = new set.type(...Object.values(set.args));그러니 돌아다니는 건 그만둬new그리고 '새로운' 형태로 일하는 것에 대해 더 많은 것을 할 수 있다.

// models/oauth.ts
export class OAuthEntity<T = string> {
  constructor(public readonly id: T) {}
  [key: string]: any;
}

export class OAuthClient extends OAuthEntity {
  /**
   * An OAuth Client
   * @param id A unique string identifying the client.
   * @param redirectUris Redirect URIs allowed for the client. Required for the authorization_code grant.
   * @param grants Grant types allowed for the client.
   * @param accessTokenLifetime Client-specific lifetime of generated access tokens in seconds.
   * @param refreshTokenLifetime Client-specific lifetime of generated refresh tokens in seconds
   * @param userId The user ID for client credential grants
   */
  constructor(
    public readonly id: string = '',
    public readonly redirectUris: string[] = [],
    public readonly grants: string[] = [],
    public readonly accessTokenLifetime: number = 0,
    public readonly refreshTokenLifetime: number = 0,
    public readonly userId?: string,
    public readonly privateKey?: string
  ) {
    super(id);
  }
}
// models/oauth.test.ts
import { makeHash, makePin } from '@vespucci/utils';
import { OAuthEntity, OAuthClient } from '@vespucci/admin/server/models/oauth';

type ModelData = { type: new (...args: any[]) => any; args: { [key: string]: any }; defs?: { [key: string]: any } };

describe('Model Tests', () => {
  const dataSet: ModelData[] = [
    { type: OAuthEntity, args: { id: makeHash() } },
    {
      type: OAuthClient,
      args: {
        id: makeHash(),
        redirectUris: [makeHash()],
        grants: [makeHash()],
        accessTokenLifetime: makePin(2),
        refreshTokenLifetime: makePin(2),
        userId: makeHash(),
        privateKey: makeHash(),
      },
    },
    {
      type: OAuthClient,
      args: {},
      defs: {
        id: '',
        redirectUris: [],
        grants: [],
        accessTokenLifetime: 0,
        refreshTokenLifetime: 0,
      },
    },
  ];
  dataSet.forEach((set) => {
    it(`Creates ${set.type.name} With ${Object.keys(set.args).length} Args As Expected`, () => {
      let model!: any;
      const checkKeys = Object.keys(set.args).concat(Object.keys(set.defs || {}).filter((k) => !(k in set.args)));
      const checkValues: any = checkKeys
        .map((key) => ({ [key]: set.args[key] || set.defs?.[key] }))
        .reduce((p, c) => ({ ...p, ...c }), {});
      expect(() => {
        model = new set.type(...Object.values(set.args));
      }).not.toThrow();
      expect(model).toBeDefined();
      checkKeys.forEach((key) => expect(model[key]).toEqual(checkValues[key]));
    });
  });
});

나에게 있어서 최종 결과는 다음과 같다.

여기에 이미지 설명을 입력하십시오.

사용할 수 있다const obj = Object.create(MyClass.prototype)그런 다음 원하는 값을 할당하십시오.Object.assign(obj, { foo: 'bar' })

이렇게 하면 클래스 인스턴스가 생성되며new키워드 또는 생성자.

스마트 인스턴스 공장(CreateCallableConstructor로 포장하는 구성자)을 만드는 @N. Kudryavtsev 솔루션을 좋아한다.그러나 간단한 Reflect.construct(타입, args)는 아그를 충분히 사용하면 완벽하게 작동한다.시제품 및 장식가에는 문제가 없음을 보여주는 mobx(v5)의 예는 다음과 같다.


import { observable, reaction } from "mobx";

class TestClass {
  @observable
  stringProp: string;

  numProp: number;

  constructor(data: Partial) {
    if (data) {
      Object.assign(this, data);
    }
  }
}

var obj = Reflect.construct(TestClass, [{numProp: 123, stringProp: "foo"}]) as TestClass;
// var obj = new TestClass({numProp: 123, stringProp: "foo"});

console.log(JSON.stringify(obj));

reaction(() => obj.stringProp, v => {
    console.log(v);
  }
);

obj.stringProp = "bar";

그리고 심지어 이 간단한 포장지 기능도 작동한다.


type Constructor = new (...args: any[]) => any;
const createInstance = (c: Constructor, ...args) => new c(...args);
var obj = createInstance(TestClass, {numProp: 123, stringProp: "foo"});

// or
const createInstance1 = (c: Constructor) => (...args) => new c(...args);
var obj1 = createInstance(TestClass)({numProp: 123, stringProp: "foo"}, 'bla');

참조URL: https://stackoverflow.com/questions/32807163/call-constructor-on-typescript-class-without-new

반응형