user3870265
user3870265

Reputation: 63

typescript how to derive type from dynamic array?

Noob with TS here. I am creating a custom Icon component in React and I want to only use Rounded Material UI icons.

I am defining the type like so:

import * as icons from '@material-ui/icons/index'
export type MaterialUiIcon = keyof typeof icons

this works but only gives me typechecking for all material UI icons.

instead this is what I want to do:

const roundedIconsNames = (Object.keys(icons).filter((icon) => icon.includes('Rounded')))
export type RoundedMaterialUiIcon = typeof roundedIconsNames[number]

however RoundedMaterialUiIcon ends up being type string[]

How can I make this work?

Thank you.

Upvotes: 1

Views: 395

Answers (1)

You are receiving string[] because Object.keys always returns string[] by design.

I believe it is better to make typeguard and utility function:

import * as icons from '@material-ui/icons/index'
type Icons = typeof icons

export type MaterialUiIcon = keyof Icons

type Prefix<T extends string> = `${string}${T}` | `${string}${T}${string}` | `${T}${string}`

// self explanatory, we have 3 variant of word
type Test1 = Prefix<'Rounded'> // `${string}Rounded` | `${string}Rounded${string}` | `Rounded${string}`


type GetByPrefix<T, P> = T extends P ? T : never;

/**
 * This will return only HelloRounded, because union 'HelloRounded' | 'Batman'
 * extends `${string}Rounded` | `${string}Rounded${string}` | `Rounded${string}`
 * 
 * WHy extends?
 * Because Rounded is at the end
 */
type Test2 = GetByPrefix<'HelloRounded' | 'Batman', Prefix<'Rounded'>> // 'HelloRounded'

/**
 * This will return "RoundedWorld" because Rounded is at the beginning
 */
type Test3 = GetByPrefix<'RoundedWorld' | 'Batman', Prefix<'Rounded'>> // "RoundedWorld"

/**
 * This is a special syntax for user defined typeguards
 * It may help you to narrow the type
 * https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
 */
const typeguard = <T extends string,>(tag: string) => (icon: MaterialUiIcon): icon is GetByPrefix<MaterialUiIcon, Prefix<T>> => icon.includes(tag)

const getRounded = <T extends string>(icons: Icons, includes: T) =>
/**
 * Object.keys will always return string[], this is by design
 * So you need to use type assertion here
 * 
 * Array.prototype.filter accept curried typeguard
 * It is a good practive yo use user defined typeguards a a predicate
 * callback in array methods
 */
    (Object.keys(icons) as Array<MaterialUiIcon>).filter(typeguard<T>(includes))

const result = getRounded(icons, 'Rounded')


Pls, keep in mind, since Icons have 5K union types, it is not easy task for TS to iterate through all unions.

Playground

Upvotes: 1

Related Questions