#Type 'string' cannot be used to index ...

40 messages ยท Page 1 of 1 (latest)

hard field
#

Hi, I'm trying to write such a code:



export type OptionValue = string | boolean | number | (string | null)

export class Option<T> {
    value: T
    constructor(value:T) {
        this.value = value
    }
}

interface GroupLike {
    options?: Record<string, Option<OptionValue>>
}

class Group<Options extends Record<string, Option<OptionValue>>> {
    options: Options
    constructor(cfg: { options: Options}) {
        this.options = cfg.options
    }

    merge<Options2 extends Record<string, Option<OptionValue>>>(
        other: Group<Options2>
    ) {
        if (other.options != null) {
            for (const [name, option] of Object.entries(other.options)) {
                const existingOption = this.options[name]
                this.options[name] = option
            }
        }
        //
    }
}


But TS tells me that options[name] = option is invalid, as options cant be indexed with string. What's interesting, the previous line works : const existingOption = this.options[name]. Would anyone be so nice and help me figure it out, please? ๐Ÿ™‚

tepid spindle
#

Hi...

hard field
#

Hi! ๐Ÿ˜„

tepid spindle
#

When you free, look at the DM

hard field
#

Oh, sorry for not replying yet! I will later today

tepid spindle
#

no worries

#

this reason is Options may be a literal object type, so TS is not sure string can use in the Options type variable

ripe heartBOT
#
Nctdt#2630

Preview:ts ... ;(this.options[ name ] as Option<OptionValue>) = option ...

hard field
#

but Options extends Record<string, Option<OptionValue>> so how it could be a literal type ?

tepid spindle
ripe heartBOT
#
Nctdt#2630

Preview:```ts
export type OptionValue =
| string
| boolean
| number
| (string | null)

export class Option<T> {
value: T
constructor(value: T) {
this.value = value
}
}

class Group<
Options extends Record<string, Option<OptionValue>>

{
options:
...```

tepid spindle
#

!ts

ripe heartBOT
#
export type OptionValue = string | boolean | number | (string | null)

export class Option<T> {
    value: T
    constructor(value:T) {
        this.value = value
    }
}

class Group<Options extends Record<string, Option<OptionValue>>> {
    options: Options
    constructor(cfg: { options: Options}) {
        this.options = cfg.options
    }

    merge<Options2 extends Record<string, Option<OptionValue>>>(
        other: Group<Options2>
    ) {
        if (other.options != null) {
            for (const [name, option] of Object.entries(other.options)) {
//                                              ^^^^^^^
// Property 'entries' does not exist on type 'ObjectConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2017' or later.
                ;(this.options[name] as Option<OptionValue>) = option
            }
        }
        //
    }
}

const foo = new Group({
    options: {
        test: new Option('')
    }
})
foo
// ^? - const foo: Group<{
//     test: Option<string>;
// }>```
tepid spindle
#

@hard fieldlike this foo

#

Options is literal object type

#

Since name is a string type, TypeScript cannot confirm if it is present as an index in this.options.

#

(May I understand is wrong

hard field
#

Wait, here options is an object and it has a field test, so we should be able to index it by field name, right?

tepid spindle
#

yep

#

but TS don't know

hard field
#

I mean, it should know if Options extends Record<string, Option<OptionValue>>, what could I put there that would not be indexable with string? I dont see it in your code

ripe heartBOT
#
Nctdt#2630

Preview:```ts
export type OptionValue =
| string
| boolean
| number
| (string | null)

export class Option<T> {
value: T
constructor(value: T) {
this.value = value
}
}
type OptionsType = Record<string, Option<OptionValue>>
class Group<
Options extends Record<string, Option<Op
...```

tepid spindle
hard field
#

Wait, but what's the diffferencebetween your code and mine? I think Im blind but I dont see any implementation idfference here

tepid spindle
#

because, I use as keyword to narrow the type

#

so TS know type is correct

#

sorry, I have finished my work, I'm going back home.

hard field
#

No worries, thanks so much. Anyway, I'm still interested in knowing why we need to narrow the type and why TS doesnt know that by itself ๐Ÿ˜ฆ

molten anchor
#

@hard field I'm thinking the root issue is probably this:

#

!*:unsafe-keys

ripe heartBOT
#
Retsam19#2505
`!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>;
molten anchor
#

name is string, and not a known key of this.options

#

Well... actually, maybe it's not that. That may be part of it, but I think there's also the issue that Options2 might have keys that aren't actually keys of this.options at all.

#

Or, worse, might be keys but have different values.

#

Consider:

ripe heartBOT
#

declare const g1: Group<{ x: Option<string> }>;
declare const g2: Group<{ x: Option<number> }>;

g1.merge(g2);
g1.options.x 
//         ^? - (property) x: Option<string>
// But it's actually Option<number> at runtime
molten anchor
#

TS is actually correct that this type, as written isn't type-safe. @tepid spindle's fix changes options to be Record<string, Option<OptionValue>>, which avoids this issue by not having the options property try to be specific about what properties it holds. Avoids the issue, but the types are less specific:

ripe heartBOT
#

declare const g1: Group<{ x: Option<string> }>;
declare const g2: Group<{ x: Option<number> }>;

g1.merge(g2);
g1.options.x 
//         ^? - Option<OptionValue>
g1.options.litearllyAnything
//         ^? - Option<OptionValue>
tepid spindle
tepid spindle