#`satisfies` changes the type?

82 messages · Page 1 of 1 (latest)

south valley
#

Hey all, this is something I stumbled upon to. It seems like an edge case, I just can't imagine why this would work this way.

Afaik, using satisfies should not change the type of the value, and yet it does very clearly in the first picture. Here's the code:

const a = <T extends 1 | 2>(b: T) => ({b}) satisfies {b: 1 | 2};
a(1).b // 1 | 2, and not the desired 1

Any clue what's up with this?

#

it seems like the satisfies keyword causes the generic type to be resolved immediately.
for instance

const a = <T extends 1 | 2>(b: T) => {
  const c = b satisfies any; // c is 1 | 2
  const d = b; // d is T extends 1 | 2
};
zealous crater
#

you don't seem to properly understand what the satifies operator does and what it tries to achieve

#

sometimes, you are working with some data
let's take an object

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ^^^^ sacrebleu - we've made a typo!
};

and you want that object to be a certain shape, enforce some kind of type safety
notice how I wrote bleu and not blue in the object?

one solution would be to create a variable, set the expected type to that variable, and then assign it a value

type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette: Record<Colors, string | RGB> = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ The typo is now correctly detected
};

problem is, we now lost some information!
we used to know the green property was a string, and the other ones were tuples
now they all are string | [number, number, number]
so we lost information, and we don't want that

we needed a new way to enforce a certain shape, without actually assigning it to a object
and that is exactly what satisfies does

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ The typo is now caught!
} satisfies Record<Colors, string | RGB>;

we can catch the typo, and we retain the fact the green property is a string and the other ones are tuples

#

@south valley

south valley
#

Im not sure why youd say that? I understand that completely.
My example is just very minimal, and not real life, but it illustrates the issue

south valley
zealous crater
#

like, there is no point in constraining data you return

#

however, you want to constrain data coming in

#

like when assigning data to a variable

south valley
#

satisfies any should be a noop essentially

formal blaze
#

@zealous crater what

#

satisfies affects inference

zealous crater
formal blaze
#

and for functions, inference from the return type is a thing

#

!:noinfer

reef runeBOT
#
Deleted User 4b32c763#0396
`!n_n:noinfer`:

Prevents inference by wrapping a type parameter in a conditional type.
Note that it won't help when the other position is always inferred after
e.g. (x: NoInfer<T>) => T

type NoInfer<T> = [T][T extends any ? 0 : never]
south valley
formal blaze
#

you can wrap this around an explicit return type (if required) to prevent this

south valley
#

Problem is that infering the return type while at the same type enforcing it is exactly what i wanted @formal blaze

formal blaze
#

that said, im not sure what the inferred return type is

#

so maybe just an explicit return ttpe is enough

zealous crater
#

wut?
T extends 1 | 2
b is T
so b is 1 | 2
c = b, so c is 1 | 2
1 | 2 satisfies any, nothing happens
working as intended
don't see the problem

south valley
#

Basically i want to make sure the return type is loosely some general type, but want it to be as narrow as possible in reality

south valley
formal blaze
#

because the input comes before everything else

#

plus there's a constraint on the input

#

so it should be checking the input is valid first, and at the same time narrowing the input if needed

#

e.g. from number -> 1

zealous crater
formal blaze
#

hm

#

c should be T, no?

south valley
#

Its not

formal blaze
#

because, yknow, b: T

south valley
#

Which is the issue

formal blaze
#

right.

south valley
#

Let me make a playground

#

One sec

reef runeBOT
formal blaze
#

oh no

reef runeBOT
#
judeh#3245

Preview:```ts
const foo = <T extends 1 | 2>(bar: T) => {
const a = bar satisfies any;
const b = bar;

// uncomment one to check the return type
return a;
// return b;
}```

formal blaze
#

was on mobile so couldn't see what was wrong

reef runeBOT
formal blaze
#

!ts

reef runeBOT
#
// 8<
const a = <T extends 1 | 2>(b: T) => ({b}) satisfies {b: 1 | 2};
//    ^? - const a: <T extends 1 | 2>(b: T) => {
//        b: 1 | 2;
//    }
  a(1).b // 1 | 2, and not the desired 1
//^? - const a: <1>(b: 1) => {
//    b: 1 | 2;
//}```
formal blaze
#

that inferred return type is...

#

very Not Good™

south valley
#

so looks like a bug? or wdym

reef runeBOT
formal blaze
#

!ts

reef runeBOT
#
// 8<
const a = <T extends 1 | 2>(b: T) => ({b: b satisfies T}) satisfies {b: 1 | 2};
//    ^? - const a: <T extends 1 | 2>(b: T) => {
//        b: T;
//    }
  a(1).b // 1 | 2, and not the desired 1
//^? - const a: <1>(b: 1) => {
//    b: 1;
//}```
formal blaze
#

this does work as a workaround though

formal blaze
#

but the idea behind satisfies is to avoid changing the type, after all...

south valley
#

yeah I noticed that too, but in my case the object is complex so the workaround isnt ideal

zealous crater
#

It's possible there are unintended downstream consequences of this (changes like this can often foul up generic inference in ways that aren't obvious), but it seems to be OK for now.

formal blaze
south valley
#

ill file the issue then, we'll see

formal blaze
#

(as normally happens to [])

zealous crater
#

think they mean generics being narrowed in general

reef runeBOT
formal blaze
#

!ts

reef runeBOT
#
// 8<
function foo<T>(_: T) {}
foo([]);
//^? - function foo<never[]>(_: never[]): void```
formal blaze
#

especially considering it's under the heading "The Empty Array Problem"

zealous crater
#

unrelated imo

#

the type for the array was a generic

#

so trying to infer that generic would cause a problem with the array losing it's type without any constraint in place on the generic

formal blaze
zealous crater
#

the fact that section is about empty arrays

#

my point is, there is something going on with generics being narrowed/infered and it can cause problems

formal blaze
#

if it really was a general remark this is where it would be:

#

well... maybe not in the TL;DR - but definitely one of the sections that isn't specifically "the empty array problem"...

zealous crater
#

¯_(ツ)_/¯

south valley
#

submitted