简体   繁体   中英

Type conversions and conditional return types in TypeScript

I am trying to convert between custom types in TypeScript and use a conditional return type. However, TS won't let me, as the following doesn't compile:

type AString = { a: string };
type ANumber = { a: number };
type A<T> = T extends string ? AString : ANumber;
function isANumber(x: any): x is ANumber {
    return x && typeof x.a === "number";
}
function isAString(x: any): x is AString {
    return x && typeof x.a === "string";
}

type BString = { b: string };
type BNumber = { b: number };
type B<T> = T extends string ? BString : BNumber;

function a2b<T>(a: A<T>): T extends string ? BString : BNumber {
    if (isAString(a)) return { b: a.a } as BString;
    if (isANumber(a)) return { b: a.a } as BNumber;
}

There is complaint about the lacking return at the end of the function, which TS could know, but that's not my pain point. More importantly, neither BString nor BNumber can be assigned to T extends string ? BString : BNumber T extends string ? BString : BNumber . Even more confusingly, both cannot be assigned to B<T> . What did I miss about conditional types?

PS: Using a union type would solve the compiler error in this example, but not in my actual code.

The problem is typescript can't make the jump in logic that isAString(a) is equivalent to checking T extends string . It narrows down the definition of the variable a but not of the generic T . It still thinks that T could be either, so it complains about returning BString or BNumber individually because it doesn't know that you are returning the right one.

Your as BString and as BNumber declarations aren't actually necessary because typescript does already understand that it is dealing with {b: string} in the first case and {b: number} in the second. The jump that it is failing to make is that these types have anything to do with T .

The easiest way to shut it up is to use as B<T> in both scenarios.

Another simple solution is to make your return type rely directly on a value which it can infer from T .

function a2b<T extends AString | ANumber>(a: T): {b: T['a']} {
    return { b: a.a };
}

If the generic T directly describes the variable a then typescript can make better inferences and will understand that if a is x than T must be x. So I try where I can to make the generic apply to the whole variable. Typescript is not the best at working backwards.

I'm still not able to get a really great solution here though. It just does not like the extends . If I say that T extends AString | ANumber T extends AString | ANumber and work backwards to get string or number for the return type, defining AType as T['a'] works fine, but if AType is T extends AString ? string : number T extends AString ? string : number then it breaks. Playground Link

It has to do with what extends really means and that literal strings and literal numbers extend string and number.

Take a look at the errors on this and I think you'll understand the problem:

function a2b<T>(a: A<T>): {b: T} {
    if (isAString(a)) return { b: a.a }; // error TS2322: Type 'string' is not assignable to type 'T'.
 // 'T' could be instantiated with an arbitrary type which could be unrelated to 'string'.
    if (isANumber(a)) return { b: a.a };
}

So we know that aa is a string and that T extends string , but we still don't know that aa is assignable to T because you could have a case where T is the literal string some string and a is {a: "other string"} .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM