#Optional keys make type not accessible by keys

1 messages ยท Page 1 of 1 (latest)

split atlasBOT
#
onkeltem#0

Preview:```ts
type FieldsGeneral = {
firstName: string
lastName: string
}

type FieldsEducation = {
university: string
}

type Fields = {
general?: FieldsGeneral // If I make these guys optional, then
education?: FieldsEducation // 23rd line below stops validatin
...```

lunar yew
#

!ts

split atlasBOT
#
type FieldsGeneral = {
  firstName: string;
  lastName: string;
};

type FieldsEducation = {
  university: string;
};

type Fields = {
  general?: FieldsGeneral;        // If I make these guys optional, then
  education?: FieldsEducation;    // 23rd line below stops validating 
};

declare const data: Fields;

function buildFields<T extends keyof Fields>(type: T) {
  return Object.keys(data[type] ?? {}).map((fieldName) => {
    // Restoring field Type
    const fieldNameTyped = fieldName as keyof Fields[T]
    const group = data[type]
//          ^? - const group: Fields[T]
    const res = group[fieldNameTyped]
//              ^^^^^^^^^^^^^^^^^^^^^
// 'group' is possibly 'undefined'.
// Type 'keyof Fields[T]' cannot be used to index type 'FieldsGeneral | FieldsEducation'.
//                ^? - const group: FieldsGeneral | FieldsEducation | undefined
  });
}
lunar yew
#

I cannot get rid of the error

#

Here is a working example, but I made the general and education keys required:

split atlasBOT
#
onkeltem#0

Preview:```ts
type FieldsGeneral = {
firstName: string
lastName: string
}

type FieldsEducation = {
university: string
}

type Fields = {
general: FieldsGeneral
education: FieldsEducation
}

declare const data: Fields

function buildFields<T extends keyof Fields>(type: T) {
return Object.keys(data[
...```

lunar yew
#

So I wonder how to properly workaround this thing with group[fieldNameTyped] not being validated

#

The error is:

Type 'keyof Fields[T]' cannot be used to index type 'FieldsGeneral | FieldsEducation'.(2536)
#

So optionating keys makes group's type to become 'FieldsGeneral | FieldsEducation'.
Otherwise it is just: Fields[T]

eternal wave
#

Reflection code is just inherently unsafe, there's no point in trying to get some halfway safety for it.

#

What's the full code of buildFields?

split atlasBOT
#
sandiford#0

Preview:```ts
type FieldsGeneral = {
firstName: string
lastName: string
}

type FieldsEducation = {
university: string
}

type Fields = {
general?: FieldsGeneral // If I make these guys optional, then
education?: FieldsEducation // 23rd line below stops validatin
...```

honest summit
#

It guess because group becomes Fields[T] | undefined it alters something

eternal wave
#

It's what most languages call what you are trying to do. Most Object methods are reflection.

honest summit
#

Yeah

#
type K = keyof (FieldsGeneral | FieldsEducation | undefined)

K is never

eternal wave
#

The usual approach to reflection code is to just, isolate the reflection logic into small pieces of functions where you can take one look at the code and know it's safe, then just assert.

honest summit
#

Oh but it's never already

eternal wave
#

!:unsafe-keys

split atlasBOT
#
retsam19#0
`!retsam19:unsafe-keys`:

Since TS allows objects to have extra properties not specified in the type, it doesn't assume that all the keys on the type are the only keys on the object. This means that Object.keys returns string[] not a specific type, and for(const key in obj), key is string, (not keyof typeof obj).

If you wish to assume otherwise, this utility is often helpful:

// A signature for `Object.keys` that assumes the only keys are the ones indicated by the type
const unsafeKeys = Object.keys as <T>(obj: T) => Array<keyof T>;
eternal wave
#

This is a classic example.

split atlasBOT
#
onkeltem#0

Preview:```ts
import {ReactNode} from "react"

interface Pbv1General {
tradingName?: string
}

interface Pbv1Financial {
bankAccountNumber?: string
}

interface Pbv1Commercial {
minimumGrossMargin?: number
}

interface Pbv1Fields {
general?: Pbv1General
financial?: Pbv1Financia
...```

lunar yew
#

Some real code

split atlasBOT
#
sandiford#0

Preview:```ts
type FieldsGeneral = {
firstName: string
lastName: string
}

type FieldsEducation = {
university: string
}

type Fields = {
general?: FieldsGeneral // If I make these guys optional, then
education?: FieldsEducation // 23rd line below stops validatin
...```

honest summit
#

strangely enough that gets rid of errors

#

Not sure why

lunar yew
#

Because there are many others ๐Ÿ™‚

#

ok, so the thing with unsafeKeys is what I sometimes use as well. But I wonder how to fix my problem, not the keys

#

but anyways, with unsafeKeys code looks more concise, so here it is:

split atlasBOT
#
onkeltem#0

Preview:```ts
type FieldsGeneral = {
firstName: string
lastName: string
}

type FieldsEducation = {
university: string
}

type Fields = {
general?: FieldsGeneral // If I make these guys optional, then
education?: FieldsEducation // 23rd line below stops validatin
...```

eternal wave
lunar yew
#

Yeah, but I also try to do it as rare as possible ๐Ÿ™‚

eternal wave
#

You are using Object.fromEntries too, and I'm assuming you want to return a strongly typed object, so you already have to assert no matter what.

lunar yew
#

true

#

Yet I still wonder how to assert it correctly, that group[fieldName] buddy

#

Maybe I can use NonNullable somewhere somehow damn it ๐Ÿ™‚

eternal wave
#

No

lunar yew
#

meh

eternal wave
#

Don't even bother trying to be cute about it, just bruteforce assert your way through.

honest summit
#
    const fieldNameTyped = fieldName as keyof Fields[keyof Fields]
    const group = data[type]
    if (group) {
      const res = group[fieldNameTyped]
    }
#

For your example works

#

Changing Fields[T] to Fields[keyof Fields] passes for whatever reason

lunar yew
#

Yeah! Magic! What the... ?

honest summit
#

He's already asserting, but was having trouble with getting an allowed key type

#

Although you can cast the object instead

eternal wave
#

That's pretty much pointless

honest summit
#

Or use any

#

And I think asserting the object or key is best. So you don't suppress real errors

eternal wave
#

The end result is that you are going to have to have asserts everywhere in this piece of code, so you have to read the code to understand what's going on.

honest summit
#

You haven't really given an alternative

eternal wave
#

So instead of muddying the entire piece of code with tons of as XYZ, you might as well just use the least amount of effort, which is just as never, and that's it.

#

You are going to have to read the entire piece of code to make sure it's safe, regardless what you are going to use.

#

Eg the original example:


function buildFields<T extends keyof Fields>(type: T) {
  const fields = data[type] ?? {}
  return Object.keys(fields).map((fieldName) => {
    const res = fields[fieldName as never]
  })
}
honest summit
#

Yeah true, key as never works

#

I forgot about that

eternal wave
#

Also gets rid of the entire const fieldNameTyped = fieldName as XYZ.

lunar yew
eternal wave
#

In my experience at least, these kinds of workaround rarely ever helps that piece of code be more safe, but makes it much harder to read.

honest summit
#

never can be assigned to anything, so it basically just forces TS to accept

lunar yew
#

I'm not sure how I can get rid of unsafeKeys and as in the function body.

honest summit
#
const res = group[fieldName as keyof Fields[keyof Fields]]
const res = group[fieldName as never]

Both ways work. And the redundant assignment isn't needed for either.

lunar yew
#

because if I do, then writing function body will be pain

eternal wave
#

This is reflection code, it's inherently unsafe. You will always have as somewhere.

lunar yew
#

So writing is harder, reading will be harder as well

split atlasBOT
#
type FieldsGeneral = {
  firstName: string;
  lastName: string;
};

type FieldsEducation = {
  university: string;
};

type Fields = {
  general?: FieldsGeneral;        // If I make these guys optional, then
  education?: FieldsEducation;    // 23rd line below stops validating 
};

declare const data: Fields;

function buildFields<T extends keyof Fields>(type: T) {
  return Object.keys(data[type] ?? {}).map((fieldName) => {
    // Restoring field Type
    const fieldNameTyped = fieldName as keyof Fields[keyof Fields]
    const group = data[type]
    if (group) {
      const res = group[fieldNameTyped]
//           ^? - const res: never
    }
  });
}
honest summit
#

I don't think you can get intellisense anyway

#

keyof Fields[keyof Fields] just resolves to never too, because there are no keys that satisfy both FieldsGeneral and FieldsEducation

eternal wave
#

Yeah you can only safely access the common fields.

honest summit
#

You can cast res to FieldsGeneral[keyof FieldsGeneral] | FieldsEducation[keyof FieldsEducation]

#

Or create a fancy type to calculate that type

lunar yew
#

yeah, I see!

#

I need to remember this as never approach

honest summit
#

It's the opposite of unknown. If you want to accept anything you can use unknown. If you want something to be accepted by anything, you can use never

honest summit
#

It's not any, because any just disables checking

#

You still have directionality with unknown and never

#

never < all types < unknown

#

< meaning smaller than / sub type

lunar yew
#

Difficult to comprehend

#

never < all types < unknown < ... here goes any?

honest summit
#

any disables checking

#

it's not in the type system

#

any means "turn off the type checker"

split atlasBOT
#
const u1: unknown = 5
const u2: unknown = 'string'
const u3: unknown = {} as never

const s1: string = 5
//    ^^
// Type 'number' is not assignable to type 'string'.
const s2: string = 'string'
const s3: string = {} as never

const n1: never = 5
//    ^^
// Type 'number' is not assignable to type 'never'.
const n2: never = 'string'
//    ^^
// Type 'string' is not assignable to type 'never'.
const n3: never = {} as never
honest summit
#

unknown is big. never is small

honest summit
#

So I can see the desire to give a type to res. Although its better not to manually create that type.

split atlasBOT
#
const a1: any = 5
const a2: any = 'string'
const a3: any = {} as never

const u: unknown = {} as any
const s: string  = {} as any
const n: never= {} as any
//    ^
// Type 'any' is not assignable to type 'never'.
honest summit
#

Oh you can't assign any to never?

#

So it's not quite types off

#

any is a weird type. I don't like it

lunar yew
#

Unfortunately I have to use both in my code:

const fieldNameTyped = fieldName as never

and

const fieldNameTyped2 = fieldName as keyof Fields[T]

as in my real code I pass a function and overrides[fieldNameTyped] becomes also never which is not a callable thing
But overrides[fieldNameTyped2] is still that function

honest summit
#

It doesn't really fit in the system clearly

lunar yew
#

Also, as I see it, const res = group?.[fieldNameTyped] always makes undefined

honest summit
#

because undefined | never is undefined

#

never is "no type", so it just kinda dissapears

lunar yew
#

Gotcha

eternal wave
#

This is why the recommended approach is to just isolate the reflection logic into a tiny part so you can write rest of the non reflection code safely. But in this case your entire logic is tangled in reflection ๐Ÿ˜…

lunar yew
honest summit
#

So my thought when I saw your code was "this is bad typescript". I.e. it's not doing things in a TypeScript friendly way.
Now don't take that as an insult to your code. Sometimes you have to write this stuff.
But my suggestion is, do make sure or think about whether there isn't a different way to go about what you're doing that is more TypeScript friendly, using more static typing

#

Honestly I don't want to sit and try to work out what your code is to see if I can be done differently ๐Ÿ˜„

lunar yew
#

I'm not sure TS is capable to help at all

#

almost any reducer makes TS useless

eternal wave
#

Not really

#

Object.fromEntries isn't safe, if you are using reducer to do the same as Object.fromEntries then that's just the same problem.

#

Otherwise Array#reduce has many safe applications, summing an array of numbers is a straight forward one.

honest summit
#

If you set out what you need to do in a general way, probably without code, someone might be able to suggest a type friendly way. I'm not sure.

lunar yew
#

In my particular case I need to rebuid a tree of data. Every project I work on I face a similar task. Every time TS goes with assertions. No way out

#

I really wished to find a less painful way

honest summit
#

Your code approach feels very much like a JavaScript dev's thinking. There is a lot of dynamic object access, that we do less of in TS

eternal wave
#

You can make that safe using the approach demonstrated by unsafe keys.

lunar yew
#

๐Ÿ™‚

honest summit
#

I keep seeing "unsafe keys" mentioned, did I miss something?

lunar yew
#

Yes ๐Ÿ™‚

#

Let me friends prepare another example please. Gimme a few minutes

eternal wave
eternal wave
honest summit
#

Ah

#

You can guarantee only the correct keys, by storing the keys you want in an array

lunar yew
#

I wished to see this in action Burrito.
I'm finishing with my example.
Very curious about Burrito's evaluation and thoughts

honest summit
#

And using some type annotations to ensure consistency

eternal wave
#

Oh? I'm not working on rewriting your code into a completely safe way, I can barely make sense of what you are trying to do with all the things like amendment missing in the class.

#

I'm just stating that general approach can go a long way.

lunar yew
#

All the comments are included ๐Ÿ™‚

eternal wave
#

Since you mentioned remapping object always end up being an assertion mess, that doesn't have to be the case

split atlasBOT
#
nonspicyburrito#0

Preview:```ts
const remapObject = <
T extends object,
U extends [PropertyKey, unknown]

(
obj: T,
fn: (
kvp: {
[K in keyof T]: [key: K, value: T[K]]
}[keyof T]
) => U
): {[P in U as P[0]]: P[1]} =>
Object.fromEntries(
Object.entries(obj).map(fn as never) as never
) as never
...```

eternal wave
#

Here's an example of isolating the unsafe mapping into remapObject, and then rest of the code can become safe. See the result is remapped from source without any assertion and fully type safe.

#

Very similar to the idea of unsafe keys.

lunar yew
#

const field2 = fieldName as keyof Fields[T]; - this line is extra

split atlasBOT
#
onkeltem#0

Preview:```ts
import {ReactNode} from "react"

// A regular real-life example

type FieldsGeneral = {
firstName?: string
lastName?: string
}

type FieldsEducation = {
university?: string
graduatedAt?: string
}

type Fields = {
general?: FieldsGener
...```

lunar yew
#

An updated version

honest summit
#

Yeah that code doesn't really seem bad to me. If it works

#

wait

lunar yew
#

waiting!

#

Ah, one more useless thing here left from the original code is <T extends keyof Fields>(type: T)
In the current version it's not needed

honest summit
#

Ah you worred about an ammendment having an extra field? I'll give you an example

lunar yew
#

This is an extended example with overrides where we can process data additionally:

split atlasBOT
#
onkeltem#0

Preview:```ts
import {ReactNode} from "react"

// A regular real-life example

type FieldsGeneral = {
firstName?: string
lastName?: string
}

type FieldsEducation = {
university?: string
graduatedAt?: string
}

type Fields = {
general?: FieldsGener
...```

lunar yew
#

And in this example I have to use two fields: fieldName as never and fieldName as keyof Fields[T]
Otherwise the overrides[field] is undefined and not a function

honest summit
#
const a = {
  amendmentId: 1,
  createdAt: '0000',
  prev: {
    general: {
      firstName: 'Bob',
      lastName: 'Smith',
      dob: '1900-01-01' // extra field
    }
  },
  next: {
    general: {
      firstName: 'Bob',
      lastName: 'Jones'
    }
  }
}
const amendment: Amendment = a
#

Here the amendment has an extra field, 'dob'

#

This would make your code crash

#

Which is why TS is making you use assertions, because it isn't really safe

lunar yew
#

Well, I'm not sure why it would crash

#

How can dob be there if it's not in the API?

#

Ah, I see. Your point is - because TS allows it...

#

right?

honest summit
#

Yeah

#

Object types are not exclusive

lunar yew
#

yep

#

I now want to see what does Burrito's remapThing bring

#

As for crashing with dob, I'm still curious why it would crash really, or rather what do you mean by the crash @honest summit ?

#

According to the business logic, we ignore prev fields that are not in the next set. But that's out of scope I guess

honest summit
#

if prev has dob and next doesn't, it will try to access next.dob, which doesn't exist

#

Well, maybe it's fine

#

because you get undefined?

#

And in your use case, undefined is OK

#

But in another situation it might not be acceptable

lunar yew
#

My code is full of ?.-guys, so theoretically it will recover ๐Ÿ™‚

#

What I've learned for the last two years with GRPC (or rather - with Proto), is that every single key is optional LOLe

honest summit
#

heh

#

sounds like hell

#

I use tRPC, it's nice

#

Things are guaranteed, mostly

#

If you do this, storing the keys in an array, then you remove the excess keys issue

#

But for a lot of code it's not a real world issue

#
// HERE
const fields = {
  general: ['firstName', 'lastName'],
  education: ['university', 'graduatedAt']
}

function buildFields<T extends keyof Fields>(type: T) {
  return Object.fromEntries(
    fields[type].map((fieldName) => { // HERE
#

I think it's that association issue that TS has that pevents the types working better

#

It is possible to write it out in a more static way, but it's a real pain... so yeah I think your code is A-OK for doing that

#

I think all your ?. put a chill up my spine, but if you need that for your environment then fair play ๐Ÿ˜†

lunar yew
# honest summit I think all your ?. put a chill up my spine, but if you need that for your envir...

I can't go w/o ?. really.

I tried to use a sort of a reasonable assumption layer in the past.
For example, we do understand, that an entity cannot come w/o id, tho in the API it's id?: number.
So we apply our knowledge at our own risk and either

  1. create our own set of types that rewrite all backend types
  2. implement some real validation with Zod, ArkType, Yup etc to ensure the backend types correspond to our assumption.
#

The bottom line: both are too much work.

honest summit
#

I mean if you have properties that might be undefined and you need to handle that, then OK

#

Does your GPRC system tell you that all types are optional?

lunar yew
#

Or it becomes an entire approach. Like you always act as you might receive null/undefeind

honest summit
#

What type knowledge do you have about the data?

lunar yew
honest summit
#

That sucks

#

What language is the backend programmed in?

lunar yew
#

Go

honest summit
#

OK

#

Advantage of a TS backend is that it's easy to send types between backend and frontend. Not so much here.

lunar yew
#

And validation as well!

#

We could have been sharing validation functions :...(

honest summit
#

I did a typesafe implementation of your code, without asserts etc

#
const fields = {
  general: ['firstName', 'lastName'],
  education: ['university', 'graduatedAt']
} as const

function buildFields(type: keyof Fields) {
  if (type === "general") {
    return Object.fromEntries(
      fields[type].map((fieldName) => { // HERE
        const prev = amendment?.prev?.[type]?.[fieldName]
        const next = amendment?.next?.[type]?.[fieldName]
        return [
          fieldName,
          {
            prev,
            next,
          },
        ];
      }),
    );
  } else if (type === "education") {
    return Object.fromEntries(
      fields[type].map((fieldName) => { // HERE
        const prev = amendment?.prev?.[type]?.[fieldName]
        const next = amendment?.next?.[type]?.[fieldName]
        return [
          fieldName,
          {
            prev,
            next,
          },
        ];
      }),
    );
  }
  const n: never = type
  throw new Error('This never occurs')
}
#

Unfortunately it's a lot of repititious code

#

And you would need to do some extra work to ensure your fields arrays matched the type, so you didn't accidently miss a field

lunar yew
#

Yeah, I see! And the fields arrays is what we should maintain manually

honest summit
#

Yeah and you need a link between fields and Fields so that match

#

a satisfies or derive one from the other

lunar yew
honest summit
#
return Object.fromEntries(
  fields[type].map((fieldName) => { // HERE
    const prev = amendment?.prev?.[type]?.[fieldName]
    const next = amendment?.next?.[type]?.[fieldName]
    return [
      fieldName,
      {
        prev,
        next,
      },
    ];
  }),
);

I wanted to put this code in a shared function, then it wouldn't be so bad. But I think that would have reintroduced the typings issues. Hopefully one day TS becomes smarter and can support this better

#

Or maybe I'm being dumb and there is a way to do it. I don't need to stress today haha

honest summit