Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'Pick' only refers to a type, but is being used as a value here when trying to extend Pick<

Tags:

typescript

I'm trying to have only some properties of ancestor exposed on my descendant. I try to achieve it through Pick

export class Base {
    public a;
    public b;
    public c;
}

export class PartialDescendant extends Pick<Base, 'a' |'b'> {
   public y;
}

but I receive two errors -

Error: TS2693: 'Pick' only refers to a type, but is being used as a value here.

and

Error:TS4020: 'extends' clause of exported class 'PartialDescendant' has or is using private name 'Pick'.

Am I doing something wrong, and is there another way to expose only chosen properties of the base class?

like image 878
Anarion Avatar asked Dec 05 '25 16:12

Anarion


2 Answers

See below for 3.0 solution

Pick is only a type it is not a class, a class is both a type and an object constructor. Types only exist at compile time, this is why you get the error.

You can create a function which takes in a constructor, and returns a new constructor that will instantiate an object with less fields (or at least declare it does):

export class Base {
    public c: number = 0;
    constructor(public a: number, public b: number) {

    }
}


function pickConstructor<T extends { new (...args: any[]) : any, prototype: any }>(ctor: T)
    : <TKeys extends keyof InstanceType<T>>(...keys: TKeys[]) => ReplaceInstanceType<T, Pick<InstanceType<T>, TKeys>> & { [P in keyof Omit<T, 'prototype'>] : T[P] } {
    return function (keys: string) { return ctor as any };
}

export class PartialDescendant extends pickConstructor(Base)("a", "b") {
    public constructor(a: number, b: number) {
        super(a, b)
    }
}

var r = new PartialDescendant(0,1);

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;
type ReplaceInstanceType<T, TNewInstance> = T extends new (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F, g: infer G, h: infer H, i: infer I, j: infer J) => infer R ? (
    IsValidArg<J> extends true ? new (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => TNewInstance :
    IsValidArg<I> extends true ? new (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => TNewInstance :
    IsValidArg<H> extends true ? new (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => TNewInstance :
    IsValidArg<G> extends true ? new (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => TNewInstance :
    IsValidArg<F> extends true ? new (a: A, b: B, c: C, d: D, e: E, f: F) => TNewInstance :
    IsValidArg<E> extends true ? new (a: A, b: B, c: C, d: D, e: E) => TNewInstance :
    IsValidArg<D> extends true ? new (a: A, b: B, c: C, d: D) => TNewInstance :
    IsValidArg<C> extends true ? new (a: A, b: B, c: C) => TNewInstance :
    IsValidArg<B> extends true ? new (a: A, b: B) => TNewInstance :
    IsValidArg<A> extends true ? new (a: A) => TNewInstance :
    new () => TNewInstance
) : never

For constructors parameters you will loose things like parameter names, optional parameters and multiple signatures.

Edit

Since the original question was answered typescript has improved the possible solution to this problem. With the addition of Tuples in rest parameters and spread expressions we now don't need to have all the overloads for ReplaceReturnType:

export class Base {
    public c: number = 0;
    constructor(public a: number, public b: number) {

    }
}

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
function pickConstructor<T extends { new (...args: any[]) : any, prototype: any }>(ctor: T)
    : <TKeys extends keyof InstanceType<T>>(...keys: TKeys[]) => ReplaceInstanceType<T, Pick<InstanceType<T>, TKeys>> & { [P in keyof Omit<T, 'prototype'>] : T[P] } {
    return function (keys: string| symbol | number) { return ctor as any };
}

export class PartialDescendant extends pickConstructor(Base)("a", "b") {
    public constructor(a: number, b: number) {
        super(a, b)
    }
}

var r = new PartialDescendant(0,1);


type ArgumentTypes<T> = T extends new (... args: infer U ) => any ? U: never;
type ReplaceInstanceType<T, TNewInstance> = T extends new (...args: any[])=> any ? new (...a: ArgumentTypes<T>) => TNewInstance : never;

Not only is this shorter but it solves a number of problems

  • Optional parameters remain optional
  • Argument names are preserved
  • Works for any number of arguments
like image 92
Titian Cernicova-Dragomir Avatar answered Dec 07 '25 17:12

Titian Cernicova-Dragomir


I am a little late to the game here, but there is an alternative and shorter way to do it if you're mainly interested in making intellisense work.

You can extend the base class and then redeclare the members you want to omit as private. This will generate a typescript error, but adding //@ts-ignore will clear it up and shouldn't affect compilation.

This is my preferred way to do it when things are simple. No real overhead here or challenging type syntax. The only real downside here is that adding //@ts-ignore above the extending class could prevent you from receiving other error messages related to incorrectly extending the Base class.

The one advantage to this approach over the accepted "pickConstructor" approach is that this method doesn't generate any extra code. Whereas "pickConstructor" literally exists as a function after compilation that runs during class definition.

class Base
{
    public name:string;   
}

// @ts-ignore
class Ext extends Base
{
    private readonly name:undefined; // re-declare
}

let thing:Ext = new Ext();
// The line below...
// Doesn't show up in intellisense
// complains about privacy
// can't be set to anything
// can't be used as an object
thing.name = "test";   // ERROR
like image 20
Spencer Evans Avatar answered Dec 07 '25 16:12

Spencer Evans



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!