#Why are those return values not matching the return type?
116 messages · Page 1 of 1 (latest)
e is narrowed, but E is not
really though you could just not encode EnvDomain as types
doesn't really make sense there
Yeah there are two things:
- I can't imagine "as a caller, passing in
'dev'and knowing the returned value has to start with'dev.'" is actually useful. - Even if it was, "function return type depending on argument type" cannot be implemented safely in TS.
That's just an example. The validation would be on the input side for validating the correct environment is passed in.
could you make a more detailed playground showing your intended usage then?
function sendTestData<E extends Env, D extends EnvDomain>(env: E, domain: D<E>) {}
The intended usage is in the playground but that's just an internal helper function that builds strings from subparts.
your playground doesn't have any usage
it's just the function decl
starting to sound like xyproblem here
It's a very simplified version for sure. Overall I want to ensure the format of the string is matching the environment.
I'll add usage to it but it'll add a lot more
Looks like I get the same problem with the higher level use attempting to narrow the return type from the generic type parameter:
Preview:```ts
// type Subdomain = ${string}.${Domain};
// type EnvSubDomain<E extends Env, D extends Subdomain> = E extends "dev"
// ? dev.${D}
// : D;
// const mydomain = "something.com";
// function getCorrectUrlForMyDomainEnv<E extends Env, D extends Subdomain>(
...```
This looks pretty related: https://stackoverflow.com/questions/75640504/is-it-possible-for-type-narrowing-to-affect-a-generic-type-parameter-within-a-ty
yeah i don't think this is a good distinction to make at type level
that aside, if there's a specific subdomain for each environment then why specify that twice?
In the real thing the environment isn't just a string. They have names and the prefix must match but there can be more levels of subdomains.
yeah i don't think this is a good distinction to make at type level
Why not?
Here I'm trying to include as much static validation as possible because most of these things can't be tested
a dev environment should mimic prod pretty closely, no?
this is creating a clear separation in logic
Closely, can't be exact in practice.
sure, but logic should be pretty much all the same
The code itself, the logic yes. But this is what manages the infrastructure and that changes in different environments.
"Function return type depending on argument type" cannot be implemented safely in TS.
Looks like this is it: https://github.com/microsoft/TypeScript/issues/33014
Wondering what kind of alternatives would provide similar functionality.
Possibly could use an exhaustive matching library to make it good enough?
You won't get dependent type like behavior with return type
Pretty sure all the matching libraries just return a union of all the branches, it won't try to match its return type with your argument type.
Which honestly, is most of the time the desired behavior.
Your original question can be solved by doing this:
Preview:```ts
type TLD = "com" | "net" | "org"
type Domain = ${string}.${TLD}
type Env = "dev" | "prod"
type EnvDomain<E extends Env, D extends Domain> = {
dev: dev.${D}
prod: D
}[E]
const getEnvDomains = <D extends Domain>(domain: D) =>
({
dev: `dev.${dom
...```
I still don't see the point though.
Maybe this use will be more compelling: #1287270865544806411 message
Say you are creating infrastructure for a beta environment but pass in domain.com, it would have to fail because you must use a beta prefix. The enforcement of prefixing beta can be done in the code itself, but if there's a reference to the created infrastructure it must be narrowed to the environment such that function createAndRunTestsButNotInProd(infrastructure: Infrastructure<"preprod">) can't be called with prod infra.
I don't see any real benefit of doing:
createInfrastructure('dev', ...)
createInfrastructure('prod', ...)
Vs simply:
createDevInfrastructure(...)
createProdInfrastructure(...)
'dev' and 'prod' are just string placeholders for the actual env object which I refrained from implementing within the playground.
They aren't really strings, they're objects with environment related information.
That doesn't really matter, you can still do createDevInfrastucture(devOnlyArgument, ...), and the function checks the argument object to be what's allowed, and returns what's allowed.
Realistically, your implementation will have both runtime logic and its return type in a ternary, losing type checking, just for the benefit of "I can import just 1 function instead of 2."
Yes but that's the same for any generic functions. You can define all functions individually but the idea is that you don't need to.
There are more than 2 environments.
No, generic functions mean to work for a wide range of types.
Your function only works for specific types, that's not very generic.
But I think we are getting off track here, we might disagree on whether that's a good idea or not, the reality is just that you cannot safely implement such a logic in TS without casting and losing type safety at some point.
There are certain forms of this problem that can be solved without losing type safety by doing the object map solution I've shown above.
this is false
same trick will work on return values
I don't see the relevance of that issue, and I think @nocturne brook is right
There aren't very many ways to make an output type change based on an input type that don't require unsafe operations like casting. (They showed the mapping approach which is the main safe approach I'm aware of)
I also agree with their broader point that splitting the function probably makes more sense - but if the convenience of one function is really important, then yeah, something like overloads or conditional types (with casting to make the implementation compile) are options.
if you check the playground link, you'll see that all you need is a discriminated union
Wait, couldn't you just create a few overloads and call it a day?
you can create overloads, or you can create a return value union and discriminate with extract and an inferred string literal type
Overload is not type safe.
Ah okay, I wasn't aware
type ReturnValue = { type: 'foo', ... } | { type: 'bar', ... };
function example<Type extends ReturnValue["type"]>(type: Type): Extract<ReturnValue, { type: Type }> { ...
this will have a conditional return type based on argument...
I had overloads functionality wrong in my head then I guess.
if you want you can even prune the discriminating key from the return type with omit
they're safe at the callsite, they're normal signatures there, but the implementation isn't checked against overload signatures, just to the implementation signature
Can you implement that safely? I feel like that's not any different from map type, even if it can.
function f(x: string): number;
function f(x: number): string;
function f(x: string | number) : string | number { return x; }
f("").toFixed(); // 💥
Right, so they're there to patch up the absense of type information in JS i guess
If I somewhat understand correctly, I might not word it correctly tho
On mobile and can't load the playground.
it depends on what you mean by "implement it safely"
in practice it is safe and convenient
whether it satisfies some formal notion of "every possible thing is checked in the impl" i neither know nor care
That's not the same as implementing safely, eg without casting/losing type safety at some point.
yeah i do not care about that in the abstract
one bit
what i care about is how error-prone the whole thing is
Because if you don't care about implementing safely, OP's question can just be solved by casting return type.
😛
OP specifically wants the implementation to be properly checked.
safe is not a dichotomy
I mean, that's what OP asked for.
my read is that the OP wants something that works reliably, not something that rigorously requires no casting
refactoring to a discriminated union resolves the type error at the cast point to something extremely narrow/descriptive
you can just expect-error it and you're fine
Preview:```ts
type TLD = "com" | "net" | "org"
type Domain = ${string}.${TLD}
type Env = "dev" | "prod"
type Return<D extends Domain> =
| {env: "dev"; domain: dev.${D}}
| {env: "prod"; domain: D}
type EnvDomain<
E extends Env,
D extends Domain
= Extract<Return<D>, {env: E}>["domain"]
...```
that TS can't narrow generics in function bodies this way is something most people know and work around without qualms because it's typically entirely localized and easy to visually verify correctness
i think it has remained this way for so long in part because it causes so little problems in practice
the two branches above tell you explicitly "whoops, this is trying to satisfy the entirety of E and not just one branch"
i would expect-error and not cast here, particularly, because we have no reason to think that the cast that works will remain correct if TS ever improves this
while we have very good reason to believe that the line may start working without a cast if they do
I've given a solution without casting at all.
at the cost of an additional indirection; either way is fine
The reason casting is a focal point, is because OP's original code already works if they just cast the return statement, so presumably they are asking because they don't want the cast.
the types work for you, not vice-versa
OP's original code works if you cast at the return statement but the structure of it is hiding a bunch of formal errors in the component types
this is what I mean by "safe is not a dichotomy"
Well this conversation is going nowhere.
for example: the OP's "domain" type is not correct
in the process of converting it to the "unsafe" union pattern, TSC was able to point me to like 3 different type errors
so i think it is very misleading to say "TypeScript cannot do this." It can, in multiple ways, with different amounts of accuracy/type-escaping required depending on how you go about it
in general I think "does this at any point require a cast" is a poor heuristic for "is this a reliable type scheme for catching errors in practice." It's not unrelated, it's just very far from the whole picture.
I'm not sure where we are going with this, I stated that return type depending on argument type cannot be implemented safely, besides some specific forms of it (which is the map type form that I've given that is actually implemented safely). You gave an example which is implemented unsafely, that doesn't refute my point.
Anyways, I think I've said all there is needed to be said.
If you have a very narrow technical definition of “safe,” sure. In practice, if you can resolve a pattern to a single type escape on a known shortcoming of the linter that is plenty safe.
We are talking past each other; I’m not disputing your claims about the formal TSC semantics; I’m saying for the purpose of writing code they’re only part of the picture
I completely agree there is safe and there is also "practical and safe enough," but that's not what OP wants. If OP wants "practical and safe enough" they literally can just cast the original code and move on, they are asking because they are presumably not satisfied with that, so giving them another answer of casting is not exactly helpful.
No, they can’t; the original code has some structural issues that make it difficult to know what you’re actually casting for
The type error it gives you is structurally useless
You can cast and force through but not all ways of doing that are created equal
Welp.
Look at the typo in the “domain” type; you can’t catch that without a different approach
You want the local error that TSC tells you at the point of casting to be only the narrowing limitation, represented directly
Yeah, can we not have a protracted argument inside a help question?