I'm working on a TypeScript service where a function returns a User | null. I want to modify the return type to be User when the throwIfNotFound parameter is set to true. This ensures an exception is thrown if the user isn't found, avoiding redundant null checks with TypeScript's strictNullChecks enabled.
Reproducible example:
interface IOptions {
throwIfNotFound?: boolean;
}
interface User {
id: number;
}
const userArray: User[] = [{ id: 1 }, { id: 2 }, { id: 3 }];
function findUserById(id: number, options: IOptions): User | undefined {
const { throwIfNotFound = false } = options;
let user: User | undefined;
if (id !== null) {
user = userArray.find((u) => u.id === id);
}
if (throwIfNotFound && !user) {
throw new Error("User not found");
}
return user;
}
const user1 = findUserById(1, { throwIfNotFound: true }); //User exists so no error is thrown and user1 isn't undefined
console.log(user1.id); // 'user1' is possibly 'undefined'.ts(18048)
Is there a way to do this using TypeScript?
Thanks
You want the output of
findUserById()to either beUserorUser | undefineddepending on the inputs. But the only way for function output types to depend on input types is if you either overload the function (giving it multiple call signatures) or make it generic. Neither method can be verified as type safe by the compiler in the implementation (it's beyond the compiler's abilities) so each approach is mostly useful from the caller's perspective.Traditionally you would overload it, like this:
The function body is only loosely checked by the compiler. If you changed the check
(throwIfNotFound && !user)to(!throwIfNotFound && !user)the compiler wouldn't be able to tell anything was wrong. So be careful.When you call an overloaded function the compiler resolves the call signature (mostly) by trying each call signature in order until it finds a match, which gives you the behavior you're looking for:
Alternatively, you could make the function generic and give it a conditional return type that depends on the input type. This approach can sometimes be preferable to overloads, especially if the input-output relationship is too complicated to be written as some small number of nongeneric call signatures.
The conditional type could look like this:
Where
FindUserById<O>will always containUser, but will either contain nothing else (thenevertype) orundefineddepending on whether the input typeOconstrained toIOptionshas atruetype forthrowIfNotFound(using the indexed access typeO["throwIfNotFound"]to look up that property).Then the function looks like
Again the compiler can't check the function body properly. Here if you wrote
return userthe compiler would complain because it can't be sure ifuseris appropriately aFindUserById<O>. So we use a type assertionas FindUserById<O>to tell the compiler we're sure we know what we're doing. Again, that's something we need to be careful about; the same issue with!throwIfNotFoundwould happen.And again, this looks good from the caller's side:
Playground link to code