Reputation: 249
I have this base case:
type Ids = "foo" | "bar" | "bazz";
interface IFoo {
foo: string;
}
interface IBar {
bar: number;
}
interface IBazz {
bazz: boolean;
}
type Ret<T extends Ids> = T extends "foo" ? IFoo :
T extends "bar" ? IBar :
T extends "bazz" ? IBazz :
never;
function a<T extends Ids>(id: T): Ret<T> {
switch (id) {
case "bar":
return { bar: 1 };
case "foo":
return { foo: "foo" };
case "bazz":
return { bazz: true };
}
}
const bar: IBar = a("bar");
const foo: IFoo = a("foo");
const bazz: IBazz = a("bazz");
As you can see, Typescript is not satisfied with my a
function implementation. What should I change to compile this function but still keep guarantees in the last three statements?
Upvotes: 0
Views: 40
Reputation: 327754
It's an open issue in TypeScript (see microsoft/TypeScript#33912) that the compiler is generally unable to verify that a particular function return value conforms to a conditional type that depends on an as-yet-unspecified generic type parameter, like Ret<T>
inside the implementation of a()
where T
is unresolved. This is related to the fact that TypeScript is unable to narrow type parameters via control flow analaysis] (see microsoft/TypeScript#24085), so checking id
with a switch
/case
statement might narrow the type of id
to, say, "bar"
, but it does not narrow the type parameter T
to "bar"
, and thus it can't guarantee that Ret<"bar">
is an acceptable output.
One thing you can do is accept that the compiler can't verify this for you and use type assertions or an overload to loosen the implementation typing enough to avoid the errors. This will work, but the compiler is not guaranteeing type safety. For example, with an overload:
function aOverload<T extends Ids>(id: T): Ret<T>;
function aOverload(id: Ids): Ret<Ids> {
switch (id) {
case "bar":
return { bar: 1 };
case "foo":
return { foo: "foo" };
case "bazz":
return { bazz: true };
}
}
now there's no error, and there's some type safety... you can't return a completely incorrect type like {spazz: true}
, but you can swap the case
s around and it won't notice:
function aBadOverload<T extends Ids>(id: T): Ret<T>;
function aBadOverload(id: Ids): Ret<Ids> {
switch (id) {
case "bazz":
return { bar: 1 };
case "bar":
return { foo: "foo" };
case "foo":
return { bazz: true };
}
}
So you have to be careful.
Another solution to this particular case is to abandon conditional types in favor of generic indexing, like this:
interface RetMap {
foo: IFoo,
bar: IBar,
bazz: IBazz;
}
function aGood<K extends keyof RetMap>(id: K): RetMap[K] {
return {
bar: { bar: 1 },
foo: { foo: "foo" },
bazz: { bazz: true }
}[id];
}
const bar: IBar = aGood("bar");
const foo: IFoo = aGood("foo");
const bazz: IBazz = aGood("bazz");
The compiler is able to verify that what we are doing here is safe, because we're indexing into an object of type RetMap
with the id
key of type K
. Oh, and if you are unhappy that this version preemptively calculates return values it won't use, you could refactor to use getters, which the compiler is also happy with:
function aGood<K extends keyof RetMap>(id: K): RetMap[K] {
return {
get bar() { return { bar: 1 } },
get foo() { return { foo: "foo" } },
get bazz() { return { bazz: true } }
}[id];
}
Okay, hope that helps you proceed; good luck!
Upvotes: 1