0
votes

I just wrote this function:

const cloneUrl = <T extends (URL | undefined)>(url: T): T => url instanceof URL
  ? new URL(url.toString())
  : undefined;

However, this has errors (or at least, errors very close to this):

'URL | undefined' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'URL | undefined'.

Type 'undefined' is not assignable to type 'T'. 'undefined' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'URL | undefined'.

Clearly I am not grasping something fundamental about TypeScript...

My goal is to make it so that when cloneUrl is called with a type known to be a URL, the return type is URL, but when cloneUrl is called with a type that is URL | undefined, the return type is URL | undefined.

Example:

class UrlWrapper {
  private readonly _urlOne: URL
  private readonly _urlTwo?: URL

  constructor(urlOne: URL, urlTwo?: URL) {
    this._urlOne = urlOne;
    this._urlTwo = urlTwo;
  }

  get urlOne(): URL { return cloneUrl(this._urlOne); }
  get urlTwo(): URL | undefined { return cloneUrl(this._urlTwo); }
}

See how urlWrapper.urlOne should always be a non-undefinable value, but urlTwo can be?

I have tried different type constraints:

  • <T extends URL>
  • <T extends URL ? URL : undefined>

I have tried different parameters:

  • url?: T
  • url: T | undefined

I've tried casting the results:

  • undefined as undefined
  • undefined as T | undefined

I've tried making the private field optional, or changing its type.

I've tried using a type checker function:

  • const checkIsUrl = (url: unknown): url is URL => url instanceof URL;

...and many more.

But there is something about how TypeScript thinks about types that I don't have the right mental model for, so I'm just not getting it.

1

1 Answers

0
votes

I dealt with this recently and found the following seems to work using generics with a conditional return type. There's the ugliness of the any use, but I'm not sure how to avoid that. It's voodoo I don't fully grok, but the types are inferred to what you expect.

export function cloneUrl<T extends URL | undefined>(value: T): T extends URL ? URL : undefined {
    return value instanceof URL ? new URL(value.toString()) : (undefined as any);
}

let source: URL | undefined = new URL("test");
cloneUrl(source); // URL
source = undefined;
cloneUrl(source); // undefined
let optionalSource: URL | undefined;
cloneUrl(optionalSource); // URL or undefined
cloneUrl(new Date()); // Nope: Date isn't a URL