Sitelet https://web.archive.org/web/20210115221441/https://github.com/microsoft/TypeScript/issues/13995
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

Generics extending unions cannot be narrowed #13995

Open
krryan opened this issue Feb 10, 2017 · 36 comments
Open

Generics extending unions cannot be narrowed #13995

krryan opened this issue Feb 10, 2017 · 36 comments

Comments

@krryan
Copy link

@krryan krryan commented Feb 10, 2017 •

TypeScript Version: 2.2.0-dev.20170126

Code

declare function takeA(val: 'A'): void;
export function bounceAndTakeIfA<AB extends 'A' | 'B'>(value: AB): AB {
    if (value === 'A') {
        takeA(value);
        return value;
    }
    else {
        return value;
    }
}

Expected behavior:
Compiles without error.

Actual behavior:

Argument of type 'AB' is not assignable to parameter of type '"A"'.

It works correctly if I just use value: 'A' | 'B' as the argument to bounceAndTakeIfA, but since I want the return value to type-match the input, I need to use the generic (overloading could do it, but overloading is brittle and error-prone, since there is no error-checking that the overload signatures are actually correct; my team has banned them for that reason). But (I'm guessing) since AB extends 'A' | 'B', narrowing doesn't happen. In reality, I am just using extends to mean AB ⊂ 'A' | 'B', but extends can mean more than that, which (I suspect) is why TS is refusing to narrow on it.

Some alternative to extends that more specifically means ⊂ (subsets maybe?) would probably be the best solution here?

@masaeedu
Copy link
Contributor

@masaeedu masaeedu commented Mar 22, 2017 •

But extends already does mean subset. If I'm not mistaken, extends is simply TypeScript's implementation of bounded quantification.

Regarding:

but extends can mean more than that

Do you have a specific example of behavior where you'd do T extends U for some T not assignable to U?

@Nimelrian
Copy link

@Nimelrian Nimelrian commented Jan 18, 2018 •

Got the same (I think) problem with a more elaborated example:

interface WithNumber {
  foo: number;
}

interface WithString {
  bar: string;
}

type MyType = WithNumber | WithString;

interface Parameter<C extends MyType = MyType> {
  container: C
} 

function isParameterWithNumberContainer(arg: Parameter): arg is Parameter<WithNumber> {
  return typeof (arg.container as WithNumber).foo === "number";
}

function test(arg: Parameter<WithNumber | WithString>) {
  if (isParameterWithNumberContainer(arg)) {
    arg.container.foo;
    return;
  }
  /*
   * Error:
   *   Property 'bar' does not exist on type 'MyType'.
   *     Property 'bar' does not exist on type 'WithNumber'.
   */
  arg.container.bar;
}

In my opinion, it is impossible that arg will be something else than a Parameter<WithString>. Typescript however still thinks that it is a Parameter<WithNumber | WithString> and thus throws an error when trying to access arg.container.bar.

@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Feb 6, 2018

TL;DR from design discussion: It's somewhat obvious what the "right" thing to do is, but would require a large rework of how we treat type parameters, with concordant negative perf impacts, without a corresponding large positive impact on the actual user-facing behavior side.

If new patterns emerge that make this more frequently problematic, we can take another look.

@lingz
Copy link

@lingz lingz commented Jun 18, 2018

Again appeared in #25039

@mifopen
Copy link

@mifopen mifopen commented Nov 16, 2018

How should I write such code then?

type NumberType = (() => number) | number;

function double<T extends NumberType>(
    num: T
) : T {
    if (typeof num === "number") return num * 2;
    return () => num() * 2;
}```

Error `Type 'number' is not assignable to type 'T'.`
@krryan
Copy link
Author

@krryan krryan commented Nov 16, 2018 •

By using return num * 2 as T; and return () => num() * 2 as T;, unfortunately. There is no better way, because while TS will narrow num it won’t narrow T for you.

@mysticatea
Copy link

@mysticatea mysticatea commented Jan 9, 2019 •

  • tsc version: 3.2.2

I have encountered this problem. It's much odd because it says "'a' does not exist" even if it's inside of if ("a" in t) block.

// Definition
type A = { a: number }
type B = { b: number }
type AorB = A | B

// Try
function f<T extends AorB>(ab: AorB, t: T): void {
    if ("a" in ab) {
        ab.a
    }
    if ("a" in t) {
        t.a // ⚠️ `a` does not exist
    }
}

Playground

@DerGernTod
Copy link

@DerGernTod DerGernTod commented Feb 6, 2019

same issue, different example:

interface AllowedMapTypings {
    'str': string;
    'lon': number;
}

const obj: AllowedMapTypings = {
    'str': 'foo',
    'lon': 123
};

function foo<T extends keyof AllowedMapTypings>(key: T): AllowedMapTypings[T] {
    return obj[key];
}
let str = foo('str'); // this works fine, str is a string

function fn<T extends keyof AllowedMapTypings>(key: string, kind: T, value: AllowedMapTypings[T]) {
    if (kind === 'str') {
        console.log(value.length); // Property 'length' does not exist on type 'AllowedMapTypings[T]'.
    }
}
@rsolomon
Copy link

@rsolomon rsolomon commented Feb 14, 2019 •

This would be great for specializing Props to React components. Contrived example:

type Type = 'a' | 'b';
type AShape = { a: 'a' };
type BShape = { b: 'b' };
type Props<T extends Type> = {
  type: T,
  shape: T extends 'a' ? AShape : BShape,
};

class Test<T extends ID> extends React.Component<Props<T>> {
  render() {
    const { type, shape } = this.props;
    switch (type) {
      case 'a':
        return <>{shape.a}</>; // Ideally would narrow `shape` here, instead of `AShape | BShape`
      default:
        return <>{shape.b}</>;
    }
  }
}

<T type="a" shape={{ a: 'a' }} /> // No error in ideal case
<T type="a" shape={{ b: 'b' }} /> // error in ideal case
@OliverJAsh
Copy link

@OliverJAsh OliverJAsh commented Mar 29, 2019

Here is an example that demonstrates the difference in behaviour between non-generic and generic functions:

type Common = { id: number };
type A = { tag: 'A' } & Common;
type B = { tag: 'B' } & Common & { foo: number };

type MyUnion = A | B;

const fn = (value: MyUnion) => {
    value.foo; // error, good!
    if ('foo' in value) {
        value.foo; // no error, good!
    }
    if (value.tag === 'B') {
        value.foo; // no error, good!
    }
};

const fn2 = <T extends MyUnion>(value: T) => {
    value.foo; // error, good!
    if ('foo' in value) {
        value.foo; // error, bad!
    }
    if (value.tag === 'B') {
        value.foo; // error, bad!
    }
};
@zakhenry
Copy link

@zakhenry zakhenry commented Jun 22, 2019

I think this is the same issue, this came from a real use case but I've reduced it to a toy example to better demonstrate:

interface Foo {
  stringVal: string;
  stringArrayVal: string[];
  numberArrayVal: number[];
}

type KeysWithType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];

type ArrayPropertyOf<T> = KeysWithType<T, Array<any>>;

type ArrayTypeOfPropertyOf<T, K extends keyof T> = T[K] extends Array<infer U> ? U : never;



/// type checks

const obj: Foo = {
  stringVal: 'hello',
  stringArrayVal: ['world'],
  numberArrayVal: [1,2,3]
}

let a: ArrayTypeOfPropertyOf<Foo, 'numberArrayVal'>; // as expected, type narrowed to number.

function printArrayKeyWorking(key: ArrayPropertyOf<Foo>) {

  switch (key) {
    case 'stringArrayVal':
    case 'numberArrayVal':
    case 'stringVal': // as expected: Type '"stringVal"' is not comparable to type '"numberArrayVal" | "stringArrayVal"'.
      console.log(key);
  }    

}

function printArrayKeyNotWorking<K extends ArrayPropertyOf<Foo>>(key: K) {

  switch (key) {
    case 'stringArrayVal':
    case 'numberArrayVal':
    case 'stringVal': // no error
      console.log(key);
  }    

}

function transformChildArrayValue<K extends ArrayPropertyOf<Foo>>(key: K, value: ArrayTypeOfPropertyOf<Foo, K>) {

  switch (key) {
    case 'stringArrayVal':
      return value.repeat(2); // not expected: Type '"stringVal"' is not comparable to type '"numberArrayVal" | "stringArrayVal"'.
    case 'numberArrayVal':
      return value ** 2; // not expected: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
    case 'stringVal':
      return key;
  }

}

Is there actually a workaround existing for this? The real case I'm having is the same kind of problem as the last function where I am unable to correctly type the key name and value as the two parameters as the type cannot be correctly narrowed. Note this is part of a library that I want to enforce good types for the downstream developer so I'm looking for generic solution not one specific to this particular problem

Any tips for a workaround greatly appreciated!

@jhnns
Copy link

@jhnns jhnns commented Jul 2, 2019 •

Not sure if that's the exact same problem, but I probably found a real simple case where this doesn't work as expected:

function ifNullThenUndefined<T>(value: T): T extends null ? undefined : T {
    return value === null ? undefined : value;
}

This complains at the return statement that Type 'undefined' is not assignable to type 'T extends null ? undefined : T'. See playground.

This, however, works:

type MapNullToUndefined<T> = T extends null ? undefined : T;

function ifNullThenUndefined<T>(value: T): MapNullToUndefined<T> {
    return (value === null ? undefined : value) as MapNullToUndefined<T>;
}

const a1 = ifNullThenUndefined(null); // a1 is inferred as undefined
const a2 = ifNullThenUndefined(undefined); // a2 is inferred as undefined
const a3 = ifNullThenUndefined(2); // a3 is inferred as a number

See playground.

@krryan
Copy link
Author

@krryan krryan commented Jul 2, 2019 •

@jhnns This is actually more closely related to another issue I’d raised, #21879. Basically, Typescript never considers the narrowing of a value, e.g. value as inplying anything about the type, e.g. T, that value has. The reason why is this:

interface A { foo: 42; }
interface B { bar: 'hello world'; }
declare function isB(value: A | B): value is B;

type Foobar<T extends A | B> = T extends A ? 'foo' : 'bar';

function demo<T extends A | B>(value: T): Foobar<T> {
    if (isB(value)) {
        return 'bar'; // errors
    }
    else {
        return 'foo'; // errors
    }
}

// here is the reason those lines error
const foo: 'foo' = demo({ foo: 42, bar: 'hello world' }); // does NOT error, but foo would be set to 'bar'

In that function call, T is A & B, which means it will pass isB and return 'bar'. But the definition of Foobar says that Foobar<A & B> should be 'foo' instead.

The “solution,” such as it is, is to use casting. Whether you do that by defining a particular type (e.g. MapNullUndefined or by just writing out as T extends null ? undefined : T, either works, but you basically are forced to tell the compiler that you have done this right, which means the compiler cannot correct you if you have not, in fact, done it right.

Which drastically limits the value of conditional types, in my opinion, since they will almost-always, necessarily, be unsafe at least within the internals of the function. And plenty of cases where A & B is never exist, which could be handled safely, but the TS team seems to be uninterested in going down that road.

@val1984
Copy link

@val1984 val1984 commented Jul 23, 2019 •

Trying to work around the fact that generic types extending union types don't get narrowed, I came up with the following code:

type Union = "A" | "B";

function differentiate(value: "A"): Array<"A">;
function differentiate(value: "B"): Array<"B">;
function differentiate(
  value: Union
): Array<"A"> | Array<"B"> {
  switch (value) {
    case "A":
      return [value];
    case "B":
      return [value];
  }
}

const arrayA = differentiate("A"); // resulting type is Array<"A"> ✅
const arrayB = differentiate("B"); // resulting type is Array<"B"> ✅

function calling(u: Union) {
  differentiate(u); // ⛔ Argument of type 'Union' is not assignable to parameter of type '"B"'.
  return u === "A" ? differentiate(u) : differentiate(u); // Works but problematic when Union includes more litterals
}

I would have expected this code to work:

type Union = "A" | "B";

function differentiate<T extends Union>(
  value: T
): Array<T> {
  switch (value) {
    case "A":
      return [value]; // value should be of type "A"
    case "B":
      return [value]; // value should be of type "B"
  }
}

const arrayA = differentiate("A"); // resulting type should be Array<"A">
const arrayB = differentiate("B"); // resulting type should be Array<"B">

function calling(u: Union) {
  return differentiate(u); // this call should be accepted
}

Thank you in advance for any input you may have!

@darkbasic
Copy link

@darkbasic darkbasic commented Feb 19, 2020

I want to implement a function which gets an ID (which can be a string or a number) or an array of IDs and coerces it into a number or an array of numbers. Basically it needs to implement the following overloads:

export function ensureIsNumber(id: string | number): number;
export function ensureIsNumber(ids: string[] | number[]): number[];

I thought of somethig like that:

function ensureIsNumber<T extends string | number, K extends T | Array<T>>(
  idOrIds: K
): K extends Array<T> ? number[] : number {
  if (idOrIds instanceof Array) {
    return idOrIds.map(id => Number(id));
  } else {
    return Number(idOrIds);
  }
}

UnfortunatelyTypescript cannot narrow it down :(
Is there any other way to implement it?

@AnyhowStep
Copy link
Contributor

@AnyhowStep AnyhowStep commented Feb 19, 2020

Add an explicit cast. Any other way you try will still be unsound, whether TS complains or not.

@krryan
Copy link
Author

@krryan krryan commented Feb 19, 2020

@AnyhowStep

Add an explicit cast. Any other way you try will still be unsound, whether TS complains or not.

Assuming (number|string) & (number|string)[] is impossible, which I think is a safe assumption, idOrIds instanceof Array does soundly imply that K is an array type, which would soundly imply that the return type should be number[]. The actual signature of the function, as written, might be flawed, but it should be possible to soundly deduce this without need for casting. And there are many similar cases.

Basically what it often comes down to is mutually-exclusive unions can (and, per this suggestion, should) be narrowed, but because of the problems when unions are not mutually exclusive, TS doesn’t. I strongly hope that someday TS will be smarter about recognizing and leveraging mutually-exclusive unions.

@AnyhowStep
Copy link
Contributor

@AnyhowStep AnyhowStep commented Feb 19, 2020 •

It's "unsound" as in TS not actually preventing you from making mistakes.

The other way to implement it would be with overloads but TS doesn't protect you from making mistakes there, either,

export function ensureIsNumber(id: string | number): number;
export function ensureIsNumber(ids: string[] | number[]): number[];
export function ensureIsNumber(idOrIds: string|number|(string|number)[]): number|number[] {
  if (idOrIds instanceof Array) {
    return 999; //not an array
  } else {
    return [111]; //not a number
  }
}

A "sound" approach would be some variant of the above that would protect you from making that mistake. As of now, no such approach exists. So, any approach is fine because they're all not great and allow you to make mistakes in your implementation.

(The best would be to not use overloads in the first place)

@darkbasic
Copy link

@darkbasic darkbasic commented Feb 19, 2020

@AnyhowStep
That's basically what I ended up implementing, but I preferred to avoid using overloads because of the issue you mentioned.

Just a couple of notes:
(string|number)[] is different than string[] | number[] because it will allow mixed types like [1, 'abc'].

Also:

function ensureIsNum(
  idOrIds: string | number | string[] | number[]
): number | number[] {
  if (idOrIds instanceof Array) {
    return idOrIds.map(id => Number(id));
  } else {
    return Number(idOrIds);
  }
}

In the previous example you won't be able to access the map method unless you cast idOrIds to Array<any>: somehow string[] | number[] is not an array for Typescript.

@AnyhowStep
Copy link
Contributor

@AnyhowStep AnyhowStep commented Feb 19, 2020 •

(string|number)[] is different than string[] | number[] because it will allow mixed types like [1, 'abc'].

Yes, but no one will see (string|number)[] from the outside as it is the implementing signature.

And I used (string|number)[] to avoid that problem you're having with ((string[])|(number[])).map()

You're interested in,

#36390
#7294

@Luxcium
Copy link

@Luxcium Luxcium commented Feb 29, 2020 •

can I throw my example (from my #36521 (comment))
in the mix asking how to solve it... I hope it will be a constructive question in regard to the problem

How can I test in order to split my code into two possibilities (like my if check on null in my code example at the bottom if (this.value !== null))

  // I wan to branch to those 2 possibilities: 
   ((value: T, index: number, array: T[]) => R
   | (value: Promise<T>, index: number, array: Promise<T>[]) => Promise<R>)

To avoid:

Argument of type 'T[] | Promise<T>[]' is not assignable to parameter of type 'T[]'.
  Type 'Promise<T>[]' is not assignable to type 'T[]'.
    Type 'Promise<T>' is not assignable to type 'T'.

Maybe my code is just wrong and I don't know

My code:

  public listMap<R>(
fn: (
      value: T | Promise<T>,
      index: number,
      array: T[] | Promise<T>[]
    ) => R | Promise<R>,
    thisArg?: any
  ) {
    if (this.value !== null) {
       return new MaybeList<R>(this.value.map<R>(fn, thisArg))
    }
    return new MaybeList<null>();
  }

this.value is defined by private value!: T[] | Promise<T>[] | null;

error message: (this.value.map())

  This expression is not callable. Each member of the union type 
  '(<U>(callbackfn: (value: T, index: number, array: T[]) => 
  U, thisArg?: any) => U[]) | 
  (<U>(callbackfn: (value: Promise<T>, index: number, array: Promise<T>[]) => 
  U, thisArg?: any) => U[])' 
  has signatures, but none of those signatures are compatible with each other.
  ts(2349)
@AnyhowStep
Copy link
Contributor

@AnyhowStep AnyhowStep commented Feb 29, 2020

A Playground would make things easier to work with.

@avonwyss
Copy link

@avonwyss avonwyss commented Jul 20, 2020

Even though it does not use generics, the following example seems to be in the same family of problems since it resembles #31743 which was closed as duplicate of this issue:

class Test {
    a: string;
    b: string;
    c: number;

    constructor() {
        this.a = "a";
        this.b = "b";
        this.c = 1;
    }

    test(key: keyof this): number {
        switch (key) {
            case "a":
            case "b":
            return this[key].length; // Property 'length' does not exist on type 'this[keyof this]'.(2339)
            case "c":
            return this[key]; // Type 'this[keyof this]' is not assignable to type 'number'.(2322)
        }
        return 0;
    }
}
@fan-tom
Copy link

@fan-tom fan-tom commented Jul 30, 2020 •

Also this issue comes up when you want to write mapper for some AST-like type:

enum TYPE {
  BOOLEAN = 'BOOLEAN',
  STRING = 'STRING',
  FLOAT = 'FLOAT',
  COMPLEX1 = 'COMPLEX1',
  COMPLEX2 = 'COMPLEX2',
}

type BooleanValue = {
  type: TYPE.BOOLEAN;
  value: boolean;
};

type StringValue = {
  type: TYPE.STRING;
  value: string;
};

type FloatValue = {
  type: TYPE.FLOAT;
  value: number;
};

type PrimitiveValue = BooleanValue | StringValue | FloatValue;

type ComplexValue1 = {
  type: TYPE.COMPLEX1;
  value: PrimitiveValue;
};

type ComplexValue2 = {
  type: TYPE.COMPLEX2;
  value: ComplexValue1;
};

type Value = PrimitiveValue | ComplexValue1 | ComplexValue2;


function mapValueGeneric<T extends Value>(value: T, f: (_: PrimitiveValue) => PrimitiveValue): T {
  switch (value.type) {
    case TYPE.BOOLEAN:
    case TYPE.STRING:
    case TYPE.FLOAT:
      return f(value);//error 2322: Type 'PrimitiveValue' is not assignable to type 'T'.
                      //error 2345: Argument of type 'T' is not assignable to parameter of type 'PrimitiveValue'.
    case TYPE.COMPLEX1:
      return {
        type: TYPE.COMPLEX1,
        value: f(value.value),//error 2322: Type '{ type: TYPE.COMPLEX1; value: PrimitiveValue; }' is not assignable to type 'T'.
      };
    case TYPE.COMPLEX2:
      return {
        type: TYPE.COMPLEX2,
        value: mapValueGeneric(value.value, f),//error 2322: Type '{ type: TYPE.COMPLEX2; value: Value; }' is not assignable to type 'T'.
      };
  }
}

function mapValueSimple(value: Value, f: (_: PrimitiveValue) => PrimitiveValue): Value {
  switch (value.type) {
    case TYPE.BOOLEAN:
    case TYPE.STRING:
    case TYPE.FLOAT:
      return f(value);
    case TYPE.COMPLEX1:
      return {
        type: TYPE.COMPLEX1,
        value: f(value.value),
      };
    case TYPE.COMPLEX2:
      return {
        type: TYPE.COMPLEX2,
        value: mapValueSimple(value.value, f),//error 2322: Type 'Value' is not assignable to type 'string | number | boolean | BooleanValue | StringValue | FloatValue | ComplexValue1'.
      };
  }
}

Playround

@Sharcoux
Copy link

@Sharcoux Sharcoux commented Aug 4, 2020 •

Just a minimal example that raises the error. I really think that it should be solve. On my case, it has a huge impact and force me to introduce a lot of assertions and type casting within my code, leading to a huge part of code not being checked correctly by the typing system...

type A = {
    testable: true
    doTest: () => void
}
type B = {
    testable: false
};

type Union = A | B

function notWorking<T extends Union,>(object: T) {
    if (!object.testable) return
    object.doTest() // Property 'doTest' does not exist on type 'T'. Did you mean 'testable'?
}

Please, note that the following function would work:

function working(object: Union) {
    if (!object.testable) return
    object.doTest()
}
@fan-tom
Copy link

@fan-tom fan-tom commented Aug 4, 2020

@Sharcoux, why you need generic type in this case, why not just accept erased type? For me it is not the best illustration of issue. This issue comes up when you want to preserve input type, so need to use it as return type (like in my example above), or if you want to have two or more function parameters of exactly same type.

@Sharcoux
Copy link

@Sharcoux Sharcoux commented Aug 4, 2020 •

What do you call erased type?
But yes, my full use case is actually type foo<T extends Union = Union> = (array: T[]) => T[] But I don't think that it changes much to the issue.

@fan-tom
Copy link

@fan-tom fan-tom commented Aug 4, 2020

@Sharcoux, when you use interface as type, not as generic's restriction (so lose information about what original type was)

@SerkanSipahi
Copy link

@SerkanSipahi SerkanSipahi commented Sep 17, 2020

@RyanCavanaugh
hi, do you have any updates about this topic?

Maybe something has changed in the two years since you wrote!

but would require a large rework of how we treat type parameters, with concordant negative perf impacts, without a corresponding large positive impact on the actual user-facing behavior side.

It would be good if you could just give us a short information. Thank you very much.

@JHawkley
Copy link

@JHawkley JHawkley commented Sep 22, 2020

It's somewhat obvious what the "right" thing to do is, but would require a large rework of how we treat type parameters, with concordant negative perf impacts (...)

@RyanCavanaugh I feel like prioritizing speed over a sound type-system is kind of a contradiction here. We use TypeScript to have a type-system. If there are large gaps in it's type-system, and I do consider this a large gap, TypeScript should correct them. Every omission like this hurts its usefulness as a tool we can rely on. It is its one, singular job to reason about types correctly.

That said: two years ago, it might've been true that this was not a common pattern. But two years later, these patterns are now everywhere. There's even examples of these kinds of generics in TypeScript's standard definitions and utility types.

The thing is, definitions don't necessarily check against an implementation. You can just say something takes a type-parameter with a union type constraint, and this issue is essentially swept under the rug from a user-facing perspective. The external API-level side all type-checks fine.

But the pattern can't be repeated in a real function implementation and have those types do their intended work. We should not need to slap // @ts-ignore - TS being dumb again to force it to swallow its own valid types.

It's really time to fix this one. At least TS has incremental compilation now, so hopefully speed isn't an issue now either.

@marcj
Copy link

@marcj marcj commented Nov 11, 2020

@RyanCavanaugh is there any plan to fix that? I encounter it a lot and it would be great if you could address that.

@kinda-neat
Copy link

@kinda-neat kinda-neat commented Nov 18, 2020

@RyanCavanaugh please consider fixing this again

@brianjenkins94
Copy link

@brianjenkins94 brianjenkins94 commented Dec 23, 2020

Could we get better compiler feedback for this issue? What I got did not lead me to think that it was a limitation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.