Typings for recursive function

Viewed 114

I'm trying to write a unwrap method of a class that holds a value, or an instance of itself with an inner value:

class Identity<T> {
  private readonly value: T;

  constructor(value: T) {
    this.value = value;
  }

  static of<T>(value: T): Identity<T> {
    return new Identity(value);
  }

  join(): T { // which return type should go here?
    if (this.value instanceof Identity) {
      return this.value.join();
    }

    return this.value;
  }
}

Here is the test:

const one = Identity.of(Identity.of(Identity.of(123)));
const result: number = one.join();
expect(result).toEqual(123);

Thanks you folks!!

2 Answers

You can change your type parameter T, so that it always is the fully nested type:

class Identity<T> {
  private readonly value: T | Identity<T>;

  constructor(value: Identity<T>);
  constructor(value: T);

  constructor(value: any) {
    this.value = value;
  }

  static of<T>(value: Identity<T>): Identity<T>;
  static of<T>(valud: T): Identity<T>;
  static of<T>(value: T | Identity<T>) {
    return new Identity(value);
  }

  join(): T {
    if (this.value instanceof Identity) {
      return this.value.join();
    }
    return this.value;
  }
}

const one = Identity.of(Identity.of(Identity.of(123)));
const result: number = one.join();

const result2 = new Identity(new Identity(new Identity(321)));
// Identity<number>

I would suggest the above method, as it seems to provide the functionality you actually want.

If you need the type T to actually be a nested Identity<Identity<...>> it is still possible, if value was public we could do the following:

class Identity<T> {
  public readonly value: T;

  constructor(value: T) {
    this.value = value;
  }

  static of<T>(value: T): Identity<T> {
    return new Identity(value);
  }

  public join(): IdentityJoinResult<T> {
    if (this.value instanceof Identity) {
      return this.value.join();
    }

    return this.value as IdentityJoinResult<T>;
  }
}

type IdentityJoinResult<T> = 
  T extends Identity<any>
    ? { [Key in keyof T]: IdentityJoinResult<T[Key]> }["value"]
    : T
;

const one = Identity.of(Identity.of(Identity.of(123)));
const result: number = one.join();

const result2 = new Identity(new Identity(new Identity(321))).join();

However if you change value to be private you will notice that you are no longer able to index by "value" in the type signature, so the last thing we can do to fix this is to defer our type logic to a separate public interface which we can determine from the type:

class Identity<T> {
  private readonly value: T;

  constructor(value: T) {
    this.value = value;
  }

  static of<T>(value: T): Identity<T> {
    return new Identity(value);
  }

  public join(): IdentityJoinResult<T> {
    if (this.value instanceof Identity) {
      return this.value.join();
    }

    return this.value as IdentityJoinResult<T>;
  }
}

interface NestedIdentity<T> {
  _value: T extends Identity<infer U> ? NestedIdentity<U> : T;
}

type NestedIdentityValue<T> = 
  T extends NestedIdentity<any>
    ? { [Key in keyof T]: NestedIdentityValue<T[Key]> }["_value"]
    : T
;

type IdentityJoinResult<T> = NestedIdentityValue<NestedIdentity<T>>;

const one = Identity.of(Identity.of(Identity.of(123)));
const result: number = one.join();
const result2 = new Identity(new Identity(new Identity(321))).join();

As Ingo mentioned in comments, infer would work if it weren't for a limitation in TS.

class Identity<T> {
  private readonly value: T;

  constructor(value: T) {
    this.value = value;
  }

  static of<T>(value: T): Identity<T> {
    return new Identity(value);
  }

  join(): T extends Identity<infer U> ? U : T {
    if (this.value instanceof Identity) {
      return this.value.join() ;
    }

    return this.value as any;

    // return this.value as T;
  }
}

const one = Identity.of(Identity.of(Identity.of(123)));
const result = one.join();
expect(result).toEqual(123);
T extends Identity<infer U> ? U : T

is the key, here.

Related