#Why are those return values not matching the return type?

116 messages · Page 1 of 1 (latest)

dusky dew
#

I was expecting that since e is narrowed to the actual value, it should be able to infer the return type correctly.

plucky bayBOT
sinful yew
#

e is narrowed, but E is not

#

really though you could just not encode EnvDomain as types

#

doesn't really make sense there

nocturne brook
#

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.
dusky dew
#

That's just an example. The validation would be on the input side for validating the correct environment is passed in.

sinful yew
#

could you make a more detailed playground showing your intended usage then?

dusky dew
#

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.

sinful yew
#

your playground doesn't have any usage

#

it's just the function decl

dusky dew
#

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

dusky dew
#

Looks like I get the same problem with the higher level use attempting to narrow the return type from the generic type parameter:

plucky bayBOT
#
tickleme_pink#0

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>(
...```

dusky dew
sinful yew
#

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?

dusky dew
#

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

sinful yew
#

a dev environment should mimic prod pretty closely, no?

#

this is creating a clear separation in logic

dusky dew
#

Closely, can't be exact in practice.

sinful yew
#

sure, but logic should be pretty much all the same

dusky dew
#

The code itself, the logic yes. But this is what manages the infrastructure and that changes in different environments.

nocturne brook
dusky dew
#

Wondering what kind of alternatives would provide similar functionality.

#

Possibly could use an exhaustive matching library to make it good enough?

nocturne brook
#

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:

plucky bayBOT
#
nonspicyburrito#0

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
...```

nocturne brook
#

I still don't see the point though.

dusky dew
#

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.

nocturne brook
#

I don't see any real benefit of doing:

createInfrastructure('dev', ...)
createInfrastructure('prod', ...)

Vs simply:

createDevInfrastructure(...)
createProdInfrastructure(...)
dusky dew
#

'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.

nocturne brook
#

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."

dusky dew
#

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.

nocturne brook
#

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.

upbeat olive
#

same trick will work on return values

fathom cargo
#

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.

upbeat olive
junior wagon
#

Wait, couldn't you just create a few overloads and call it a day?

upbeat olive
#

you can create overloads, or you can create a return value union and discriminate with extract and an inferred string literal type

nocturne brook
junior wagon
#

Ah okay, I wasn't aware

upbeat olive
#
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...

junior wagon
#

I had overloads functionality wrong in my head then I guess.

upbeat olive
#

if you want you can even prune the discriminating key from the return type with omit

sinful yew
nocturne brook
plucky bayBOT
#
function f(x: string): number;
function f(x: number): string;
function f(x: string | number) : string | number { return x; }
f("").toFixed(); // 💥
junior wagon
#

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

nocturne brook
#

On mobile and can't load the playground.

upbeat olive
#

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

nocturne brook
#

That's not the same as implementing safely, eg without casting/losing type safety at some point.

upbeat olive
#

yeah i do not care about that in the abstract

#

one bit

#

what i care about is how error-prone the whole thing is

nocturne brook
#

Because if you don't care about implementing safely, OP's question can just be solved by casting return type.

junior wagon
#

😛

nocturne brook
#

OP specifically wants the implementation to be properly checked.

nocturne brook
#

I mean, that's what OP asked for.

upbeat olive
#

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

plucky bayBOT
#
ultrafilter#0

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"]
...```

upbeat olive
#

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

nocturne brook
#

I've given a solution without casting at all.

upbeat olive
#

at the cost of an additional indirection; either way is fine

nocturne brook
#

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.

upbeat olive
#

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"

nocturne brook
#

Well this conversation is going nowhere.

upbeat olive
#

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.

nocturne brook
#

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.

upbeat olive
#

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

nocturne brook
#

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.

upbeat olive
#

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

nocturne brook
#

Welp.

upbeat olive
#

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

fathom cargo
#

Yeah, can we not have a protracted argument inside a help question?