skip to Main Content

I have a component that accepts an array of keys, and then passes one of those keys to the children render prop.

type PagerKeys = Readonly<[string, string, ...string[]]>; // needs to be at least 2 items

interface PagerProps<Keys extends PagerKeys> {
    children: (
        props: {
            currentKey: Keys[number]
        }
    ) => ReactNode;
    keys: Keys;
}

function Pager<Keys extends PagerKeys>({
    children, keys
}: PagerProps<Keys>) {
    const [currentKey, setCurrentKey] = useState<Keys[number]>(keys[0]);

    return children({
        currentKey
    });
}

I’m able to type check if the currentKey in the render prop matches keys by adding as const to the keys prop. (PagerKeys above is Readonly for that reason).

<Pager keys={['a', 'b'] as const}>
    {({currentKey}) => {
        if (currentKey === 'a') {}
        if (currentKey === 'lalala') {} // throws error as it should because 'lalala' is not a key
        ...
    }}
</Pager>

I’m wondering if there’s a way to infer the string literal in keys without adding as const?

2

Answers


  1. TypeScript 5.0 introduced const type parameters that let you add a const modifier to type parameters to ask the compiler for the same sort of inference you’d get if someone used a const assertion. This makes it easy for generic function implementers to ask for as const-like behavior without having to use more obscure tricks (such as those described in microsoft/TypeScript#30680).

    In your case the change is as simple as

    function Pager<const K extends PagerKeys>({
        // ------> ^^^^^
        children, keys
    }: PagerProps<K>) {
        const [currentKey, setCurrentKey] = useState<K[number]>(keys[0]);
    
        return children({
            currentKey
        });
    }
    

    And now when you call

    <Pager keys={['a', 'b']}>
        {({ currentKey }) => {
            if (currentKey === 'a') { }
            if (currentKey === 'lalala') { } // throws error as it should because 'lalala' is not a key
            return null!
        }}
    </Pager>
    

    The compiler infers the type parameter as readonly ["a", "b"] as desired:

    // (property) PagerProps<readonly ["a", "b"]>.keys: readonly ["a", "b"]
    

    Playground link to code

    Login or Signup to reply.
  2. Pre-5.0, this was still possible using variadic tuple types to infer the strings as literal types:

    interface PagerProps<Keys extends PagerKeys> {
        children: (props: { currentKey: Keys[number] }) => any;
        keys: [...Keys]; // ✨
    }
    

    This is the only change necessary!

    Playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search