type narrowing for only allow attributes 'keyof T' of certain type

562 views Asked by At

Trying to make keyof T to be of string return type and it seems to work when trying to pass the attribute into tags. Though when using it as an index it seems not to narrow it down that its going to be a string. Error: Type 'T[KeyOfType<T, string>]' is not assignable to type 'string'. Am I doing something wrong or is it typescript limitations?

type KeyOfType<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];


interface TagProps<T> {
    tags: T[];
    tagLabelKey: KeyOfType<T, string>;
}

const tag = [{
    id: 1,
    name: 'raw',
}]

const tags = <T,>({tags, tagLabelKey}: TagProps<T>) => {
    const getLabel = (tag: T): string => {
        return tag[tagLabelKey];
    }
}

tags({ tags: tag, tagLabelKey: 'name' })

playground

1

There are 1 answers

0
mihi On BEST ANSWER

This indeed seems to be a typescript limitation. From one of the typescript developers on this github issue:

The problem here is that we don't have the higher-order reasoning to say "If a key came from an operation that filters out keys that aren't strings, then indexing by that key must have produced a string". The FilterProperties type is effectively logically opaque from TS's point of view.

He even presents an alternative way of achieving the same semantics. Translated into your situation (and with the assumption that you want to map getLabel over the array and return the results):

interface TagProps<P extends string> {
    tags: Record<P, string>[];
    tagLabelKey: P;
}

const tags = <P extends string,>({tags, tagLabelKey}: TagProps<P>) => {
    const getLabel = (tag: Record<P, string>): string => {
        return tag[tagLabelKey];
    }
    return tags.map(getLabel)
}

const tag = [{
    id: 1,
    name: 'raw',
}]

tags({ tags: tag, tagLabelKey: 'name' }) // typechecks
tags({ tags: tag, tagLabelKey: 'id' }) // does not typecheck

The high-level difference is that instead of being generic in the tag object type and extracting the string keys (via KeyOfType), be generic in the key and use it built up the relevant part of the tag object while only allowing strings to be assigned to it (via Record<P, string>).

Or with fewer explicit types and more type inference:

interface TagProps<P extends string> {
    tags: Record<P, string>[];
    tagLabelKey: P;
}

const tags = <P extends string,>({tags, tagLabelKey}: TagProps<P>): string[] => {
    return tags.map((tag) => tag[tagLabelKey])
}