#Object type inference help

166 messages · Page 1 of 1 (latest)

humble reefBOT
#
strobe#9977

Preview:```ts
type TList<P extends string = string> = Record<Uppercase<${P}${string}>, string>

const items = {
A: 'aaa',
B: 'bbb',
C: 'ccc'
} satisfies TList

function foo1(a: TList) {
return a
}

function foo2<
Input extends TList

(a: Input) {
return a
...```

red merlin
#

questions are in the code

tulip wasp
#

it's not about the inference of Prefix, it's about the inference of Input

#

Prefix always gets inferred as 'TEST_', but Input in foo3 doesn't exist, and input is set to be TList<Prefix>
in this context, TS disallows excess properties, as said in the error
in foo4, Input is some subtype of TList<'TEST_'>, which { BAD_PREFIX_A: string } is

#

TList doesn't seem to be.. really doing anything useful, what's it supposed to do?

#

since items isn't relevant to TList, only the inverse, this

// This should ideally be { TEST_A: string } etc
```just doesn't make sense
red merlin
tulip wasp
#

A, B, C, items never come up in the definition of foo3 or TList, there's just no logical connection to use those values

red merlin
#

foo3 and foo4 aren't using items, but they care about the 'shape' of TList

tulip wasp
#

right, but TList doesn't mention A either

#

there's just nowhere for those values to come from

red merlin
#

i'm really not on the same page as you, could you try rewording your explanation, i'm pretty confused

tulip wasp
#

ok so. where would A come from for foo3's return type

#

it has to come from somewhere if you want it to be in the return type

red merlin
#

oh you're saying because it's not a parameter on the function it can't be inferred

#

yeah the point of those two functions was to demonstrate what i wanted

#

i'm not sure how to combine the ideas

tulip wasp
#

i still don't know what you actually want

#

or what TList is supposed to do

red merlin
#

well the example is just for learning purposes

#

but TList is supposed to be a record with uppercase string keys, and key values

#

with an optional prefix

#

so the intent is to pass in some value with a prefix, and if it has a bad prefix you get the TS error

#

and the type you get back is the value you passed in

#

if i understand correctly you've said Input extends TList<Prefix> is doing something funky

tulip wasp
#

wouldn't that just be foo3

tulip wasp
red merlin
#

ok, but i need the value, since if i take the example i'm working on here into the real world, the TList<Prefix> wouldn't work

tulip wasp
#

"the value" which one?

#

could you be more specific

red merlin
#

whatever is in the input of the object

#

as a qualified type

#

so { foo: string; bar: string } etc

tulip wasp
#

well that would require it be generic and for that it would also be extensible

red merlin
#

which is foo4 again

#

and that loses the error if the prefix is incorrect, it just slides through

#

in case it's not clear BAD_PREFIX_A should error

#

if the prefix doesn't match

#

i'd have thought it's something TS could achieve since it's evaluating at the time of function call using the function generics

humble reefBOT
#
that_guy977#0

Preview:```ts
type TList<P extends string = string> = Record<
Uppercase<${P}${string}>,
string

function foo1<
Prefix extends string,
Input extends TList<Prefix>

(a: {
prefix: Prefix
input: {
[K in keyof Input as K extends ${Prefix}${string}
? K
: never]: Input[K]
}
})
...```

vague valve
#
type Test = { BAD_PREFIX_A: 'aaa' } extends Record<Uppercase<`TEST_${string}`>, string> ? true : false
#

!ts Test

humble reefBOT
#
type Test = true /* 1:6 */```
tulip wasp
#

yeah this is the issue with using a generic

vague valve
#

That bad object is still assignable because excess properties are irrelevant, so it's essentially {} extends ....

red merlin
#
  input: {
    [K in keyof Input as K extends `${Prefix}${string}`
      ? K
      : never]: Input[K]
  }

could you explain why you did that

vague valve
#

But if you want the bad prefix to also error, you are looking for a validator pattern.

tulip wasp
red merlin
#

directly annotated?

tulip wasp
#

input: { ... }

red merlin
#

why does the directness matter?

tulip wasp
#

because with a generic, it can be extended

red merlin
#

hm, but wouldn't the fact you're adding never still cause it to error?

tulip wasp
#

as never in a mapped type's key removes the key

red merlin
#

oh i missed that, i misread it as foo: never

#

i didn't know you could do that

#

so in foo4

function foo4<
    Prefix extends string,
    Input extends TList<Prefix>
>(a: { prefix: Prefix, input: Input }) {
    return a.input
}
#

could i ask for a walkthrough of what the TS compiler is thinking?

tulip wasp
#

oh, im not qualified to do that lol

red merlin
#

i don't mean like AST

tulip wasp
#

disallowing excess properties is the exception, not the rule
only when an object literal is assigned to a concrete type are they disallowed
otherwise, you can use subtypes freely

red merlin
#

Input extends TList<Prefix> so TList is extendable here... meaning i can add any key to the object?

tulip wasp
#

Input is extensible from TList<Prefix>

#

but yes that's what it means

red merlin
#

so there are two issues working together

  1. i needed to use a generic to carry the inference through the function call
  2. using a generic means using extends which opens up excess keys
tulip wasp
#

yeah

red merlin
#

ok i'm happy i understand that so far

#

so because of 1,2 above, the only solution is to eliminate the bad keys to trigger an error, which means a validator type, or a directly annotated type?

#

a validator type would be Input extends Validate<TList<Prefix>>?

tulip wasp
#

im not too familiar with them, sorry
you'll have to ask @vague valve for that (or wait for someone else)

red merlin
#

there's a lot of terminology flying around, as you can see breaking it down into smaller pieces is helping me quite a bit

humble reefBOT
#
nonspicyburrito#0

Preview:```ts
type ShowInputError<T, P extends string> = {
[K in keyof T]: K extends Uppercase<${P}${string}>
? T[K]
: never
}

type HasBadKey<T, P extends string> = {
[K in keyof T]: K extends Uppercase<${P}${string}>
? never
: true
}[keyof T]

type ValidateInput<T, P extends string> = HasBadKey<
T,
P

extends true
? ShowInputError<T, P>
: T
...```

vague valve
#

Here's one with validator pattern, which will error on the bad prefixes.

red merlin
#

i need to figure out how to write this into TSDoc once i've figured the issue out too

vague valve
#

If you don't care about having good errors you can remove the ShowInputError part and just replace it with never.

red merlin
#

what makes a good error?

#

i'll check back in about an hour, i have a bad headache and need to close my eyes for a bit. thank you both so far, the input has been great

vague valve
#

Oh huh, seems like TS inference is smart enough that the whole check can just be removed.

humble reefBOT
#
nonspicyburrito#0

Preview:```ts
type ValidateInput<T, P extends string> = {
[K in keyof T]: K extends Uppercase<${P}${string}> ? T[K] : never
}

function test<P extends string, T>(a: { prefix: P; input: ValidateInput<T, P> }): T {
return a.input
}

test({ prefix: 'TEST_', input: { TEST_A: 'aaa' } }) // should pass
...```

vague valve
#

But validator pattern usually boils down to this:

type Validate<T> = ... extends ... ? T : X

function foo<T>(arg: Validate<T>)

Where the extends check validates T to satisfy the shape you want it to be, ? T ensures TS will infer T based on that argument, and : X is used to provide a good error.

red merlin
#

are you able to extend from a validator or does it need to be an annotated tye

red merlin
tulip wasp
#
const x: T = { /* excess properties disallowed */ }
```something like this
red merlin
#
type Validate<P extends string, T> = {
    [K in keyof T]: K extends `${P}${string}` ? T[K] : `Must be prefixed with ${P}`
}

function foo5<
    Prefix extends string,
    Input extends Validate<Prefix, any>
>(a: { prefix: Prefix, input: Input}) {
    return a.input
}
const result5 = foo5({ prefix: 'TEST_', input: { aaa: 'zzz'  })

this doesn't work and i can't really see the issue, i'm also not super happy about any

vague valve
#

You are doing it wrong

tulip wasp
#

well that's certainly a valid summary of what i was about to write lmao

vague valve
#

Should be:

function foo5<
    Prefix extends string,
    Input
>(a: { prefix: Prefix, input: Validate<Prefix, Input>}) {
    return a.input
}
red merlin
#

so Validate can't be in the generic part

vague valve
#

No.

red merlin
#

the return type is Validate<'THE PREFIX', { foo: string; }>

#

is it possible to reduce that

vague valve
#

You can just slap on : Input as the return type.

red merlin
#

i've been aiming to do it without adding a return type, is that not possible with validators?

vague valve
#

Usually no.

#

Validator pattern is kind of a "yes you can do it but most of the time you probably shouldn't" kind of deal.

humble reefBOT
#
strobe#9977

Preview:```ts
type Validate<P extends string, T> = {
[K in keyof T]: K extends ${P}${string}
? T[K]
: Must be prefixed with ${P}
}

function foo5<Prefix extends string, Input>(a: {
prefix: Prefix
input: Validate<Prefix, Input>
}): Input {
return a.input
...```

vague valve
#

Yeah you'd need to cast the return too if you are mapping that way to get better error messages.

red merlin
#
function foo5<
    Prefix extends string,
    Input
>(a: { prefix: Prefix, input: Validate<Prefix, Input>}): Input {
    return a.input as Input
}

so like that? it feels pretty hacky

vague valve
#

Yes.

#

You don't need the : Input if you are casting

#

You don't need the : Input or the casting if you don't use the error message.

red merlin
#

what do you mean by not using the error message

vague valve
#

Just replace your Must be... part with never.

humble reefBOT
#
strobe#9977

Preview:```ts
type Validate<P extends string, T> = {
[K in keyof T]: K extends ${P}${string}
? T[K]
: never
}

function foo5<Prefix extends string, Input>(a: {
prefix: Prefix
input: Validate<Prefix, Input>
}) {
return a.input
}
const result5 = foo5({
prefix: "TEST_",
input: {TEST_aaa: "zzz"},
})
...```

red merlin
#

the return type is the validate again that way

vague valve
#

Oh derp, yeah you still need the : Input

#

But you don't need the cast.

red merlin
#

i see. how come that affects it?

vague valve
#

Because never is assignable to anything.

#

Technically you also don't need to do : Input either

#

Validate<Prefix, Input> should have the exact same shape as Input if it passes the validation, but the type will look a bit ugly.

vague valve
#

Yeah that basically forces an expansion.

humble reefBOT
#
nonspicyburrito#0

Preview:```ts
type Validate<P extends string, T> = {
[K in keyof T]: K extends ${P}${string}
? T[K]
: Must be prefixed with ${P}
} & {}

function foo5<Prefix extends string, Input>(a: {
prefix: Prefix
input: Validate<Prefix, Input>
}) {
return a.input
...```

vague valve
#

You can also force the expansion by adding & {}.

red merlin
#

what exactly is meant by expansion

#

how does & {} on the validator cause the type to expand?

vague valve
#

& {} works on everything, it's a common trick to expand a type.

red merlin
#

how would you go about expanding that? since it just wrapped it further

vague valve
#

Expanding what? The playground seems to work fine.

red merlin
#

L14

#

hmm

#

link isn't updating

#

weird

humble reefBOT
#
strobe#9977

Preview:```ts
type Validate<P extends string, T> = {
[K in keyof T]: K extends ${P}${string}
? T[K]
: Must be prefixed with ${P}
}

type Expand<T> = T & {}

function foo5<Prefix extends string, Input>(a: {
prefix: Prefix
input: Expand<Validate<Prefix, Input>>
})
...```

red merlin
#

there

vague valve
#

The usual way to do expand is:

type Expand<T> = {
    [K in keyof T]: Expand<T[K]>
} & {}
#

But I think it doesn't work in this case because it breaks the inference of validator.

tulip wasp
#

there's also this

#

!:expand

humble reefBOT
#
gerrit0#0
`!gerrit0:expand`:
type Expand<T> = T extends Function ? T : { [K in keyof T]: Expand<T[K]> };

interface Foo<T> {
    foo: T
}
interface Bar {
    a: Foo<1>
    b: Foo<string>
    fn(): string
}
interface Zip {
    c: boolean
}
type Collapsed = (Bar & Zip)[]
//   ^? - type Collapsed = (Bar & Zip)[]
type Expanded = Expand<Collapsed>
//   ^? - type Expanded = {
//       a: {
//           foo: 1;
//       };
//       b: {
//           foo: string;
//       };
//       fn: () => string;
//       c: boolean;
//   }[]
vague valve
#

Oh nice, seems like that version of expand works.

red merlin
#

why T extends Function?

#

also, is that type recursive?

#

it's self referencing, isn't that a nono?

red merlin
#

say you have Record<string, boolean>

#
type Validate<T> = { [K in keyof T]: K extends `foo_${string}` ? T[K] : 'foo prefix missing' }

something like this would not be expected to work i believe, due to the 'foo prefix missing' attempting to replace boolean?

vague valve
#

It will work just fine.

#

And you want the error type to be completely incompatible with the original type.

#

The idea is that, if validator receives { foo: string, bar: number } and you want it to error on bar because validation failed, then your validator should return a type that's different only on bar, for example { foo: string, bar: 'bar must be string' }

#

This way TS will report "bar: number is not assignable to bar: 'bar must be string'"

#

A lazy way to do it is to simple make it return { foo: string, bar: never }.

red merlin
#

ok and what if you have a type that depends on Record<string, boolean> but wrap a function argument with the validator logic?

#

it seems to cause the function internals to error since the type doesn't align

vague valve
#

Yes, that's why validator pattern kind of sucks and you shouldn't use it in most situations.

#

You will not have a fun time inside the function implementation.

humble reefBOT
#
strobe#9977

Preview:```ts
type Validate<P extends string, T> = {
[K in keyof T]: K extends ${P}${string}
? T[K]
: Must be prefixed with ${P}
}

type MyRec = Record<string, boolean>

function foo({
record,
}: {
record: Validate<"Foo", MyRec>
}) {
return parseRec(record)
}
...```

red merlin
#

swapping the falsy type to never fixes it

vague valve
#

That's not the right way to use validator pattern, you need a generic.

#

You want:

function foo<T extends MyRec>({ record }: { record: Validate<'Foo', T> })
red merlin
#

ah thanks for pointing that out, it's good to reinforce it

#

the issue/fix is still the same though as i understand

vague valve
#

Yeah.

red merlin
#

so would using never here be considered ok even though you said it's a lazy error message, since you get this if you don't expand

input.tsx(16, 15): The expected type comes from property 'hello' which is declared here on type 'Validate<"Foo", { hello: boolean; }>'

#

assuming Validate has a more appropriate name, like CheckPrefixKey<"Foo", { hello: boolean }>

vague valve
#

Yeah it's fine, up to you really.

red merlin
#

so continuing this, let's say the validator is expanded

#

Type 'boolean' is not assignable to type 'never'.(2322)
input.tsx(16, 15): The expected type comes from property 'hello' which is declared here on type '{ hello: never; }'

#

would you consider this problematic as it doesn't hint to the underlying issue

#

you'd have to dig into the code

#

example here

humble reefBOT
#
strobe#9977

Preview:```ts
type Validate<P extends string, T> = {
[K in keyof T]: K extends ${P}${string}
? T[K]
: never
} & {}

type MyRec = Record<string, boolean>

function foo<T extends MyRec>({
record,
}: {
record: Validate<"Foo", T>
}) {
return parseRec(record)
}

function parseRec(a: MyRec)
...```

vague valve
#

It's entirely up to you, if you think it's ambiguous you can add proper error messages

#

But also keep in mind in real code your function will likely not named foo.

#

The function name, its other arguments, or just naming Validate<P, T> better (eg ValidatePrefix) might already be enough.