Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Pick" or "Exclude" constructor from class type #29261

Open
4 of 5 tasks
lsagetlethias opened this issue Jan 4, 2019 · 9 comments
Open
4 of 5 tasks

"Pick" or "Exclude" constructor from class type #29261

lsagetlethias opened this issue Jan 4, 2019 · 9 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@lsagetlethias
Copy link

lsagetlethias commented Jan 4, 2019

Search Terms

intersect, type, intersect constructor, pick new, pick constructor

Suggestion

class C1 {
  static S1 = 'foo';
  bar = 'bar';
  constructor(p1, p2, p3) {}
}
type T1 = Pick<typeof C1, new>;
// could simply be also:
type Only<T, K extends keyof T> = Pick<T, { [P in keyof T]: P extends K ? P : never }[keyof T]>;
type T2 = Only<typeof C1, 'constructor'>;

const clazz: T1 = null; // `null` could be anything
new clazz(); // should throw error because of missing params
new clazz(null, null, null); // should work
new clazz(null, null, null).bar; // should throw error because `bar` doesn't exist
clazz.S1; // should throw error because `S1` doesn't exist

Use Cases

It could be useful in class type intersection and/or for multiple inheritance (like PHP Trait, or Java default methods in interface).
And make mixins more "clean" imho.
One of my goal is to make a "type mixin" without a "value mixin".

Examples

For example:

class C1 {
    public bar: string;
    constructor(p1: string) {}
    public foo() {}
}

class C2 {
    public constructor() {}
    public baz() {}
}

type T1 = typeof C1 & typeof C2;

const a: T1 = null;
a.prototype.foo(); // ok
a.prototype.bar; // ok
a.prototype.baz(); // ok
new a(); // ok (return C2)
new a(''); // ok (return C1)
new a().foo() // error
new a('').baz(); // error

type T2 = typeof C1 & Exclude<typeof C2, new>

const b: T2 = null;
b.prototype.foo(); // ok
b.prototype.bar; // ok
b.prototype.baz(); // ok
new a(); // error <<<
new a(''); // ok (return C1 with C2 prototype)
new a('').foo() // ok (from C1)
new a('').baz(); // ok (from C2) <<<

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@weswigham
Copy link
Member

Ahhh so you want to be able to pull the signatures out of a type.

@weswigham weswigham added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Needs Investigation This issue needs a team member to investigate its status. labels Jan 4, 2019
@lsagetlethias
Copy link
Author

Well it could be a way to see it.
My need is only to think of the constructor as a named part of the prototype (like other properties and methods).

But blindly extract a signature out of a function could be a nice feature to!

@fatcerberus
Copy link

Technically ClassName.prototype.constructor === ClassName so it’s already a named method but I don’t think the type system sees it as such.

@lsagetlethias
Copy link
Author

lsagetlethias commented Jan 8, 2019

Yeah sure, that's why I don't talk about constructor in a "prototype" way, but new as a method in a "class" way (even if, ok, class is only syntactic sugar)

@fatcerberus
Copy link

To be fair, you did say:

My need is only to think of the constructor as a named part of the prototype

😉

But I know what you meant, no worries.

@Jessidhia
Copy link

Jessidhia commented Jan 10, 2019

Right now any type mapping completely destroys any call or construct signatures. This is probably related to TypeScript not quite having higher order function types.

You can get at the constructor itself by using typeof ClassName, but that also includes all static members and the things that come from Function.prototype.

(also, it would be nice indeed if TypeScript would synthesize a .constructor for instances that point to the constructor type. Right now the .constructor is just inherited from Object.prototype and is of type Function.)

@lsagetlethias
Copy link
Author

lsagetlethias commented Jan 15, 2019

You can get at the constructor itself by using typeof ClassName, but that also includes all static members and the things that come from Function.prototype.

Yeah, because Typescript prefer returning the Function object instead of the actual signature.

They are basically the same in js but not in ts:

let A = function(foo: string) {
    this.bar = function(n: number) {
        return 'baz';
    }
}

let B: typeof A; // B type recognize as "(foo: string) => void"
let B2: InstanceType<typeof B>; // error

let C = class {
    constructor(foo: string) {}
    bar(n: number) {
        return 'baz';
    }
}

let D: typeof C; // D type is recognize as "typeof C"
let D2: InstanceType<typeof D>; // ok

let bInstance = new B('qux');
bInstance.bar(); // should have an error but bInstance is any because constructor of A and B is new()=>any be default
let dInstance = new D('quw');
dInstance.bar(); // error but it's ok

Playground

If a class is really a form of type by itself, it should be tweakable. We should be able to use new operator as type and insert a constructor to anything or also remove it from anything.

@kiprasmel
Copy link

kiprasmel commented Jan 28, 2020

bump - need this.

I'd use the never type, but the auto-completions still show up for the property, and I don't want that.

Also, once I want to create a new class & the parameter I want gone is not provided, typescript errors, because the parameter is required, even though it's never...

I'd also like to be able to do something like:

export class UniqueLesson extends NonUniqueLesson implements Omit<NonUniqueLesson, "nonUniqueId"> {
	uniqueId: string;
}

to then explicitly create a unique id for the unique lesson.

BUT I still have the nonUniqueId auto-completion shenaningans etc.

@gerarts
Copy link

gerarts commented Aug 5, 2020

For those that are looking for a solution to patch / replace a constructor in a class, this works for me.

interface Cat {}

class Duck {
    /* This should make a Duck instance when constructed */
    
    public static quack: string = "QUACK!";
}

type NotDuck = (new() => Cat) & typeof Duck; // The order here matters

const nonDuck: NotDuck = <NotDuck>Duck;

const soundsLike = nonDuck.quack; // We can still see Duck's static
const whoami = new nonDuck(); // but TypeScript sees this instance as Cat

If you want to add Cat to Duck you can also just

interface Cat {}

class Duck {
    /* This should make a Duck instance when constructed */
}

type NotDuck = (new() => Cat & Duck) & typeof Duck; // The order here matters

const nonDuck: NotDuck = <NotDuck>Duck;

const whoami = new nonDuck(); // TypeScript sees this as Cat & Duck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants