Typescript: Narrowing a function/component type to take a more specific parameter

Created on 6 Mar 2020  ·  3Comments  ·  Source: microsoft/TypeScript

I’m trying to find a way to support typings for cross-platform React Native components in a single file using already supported TS features.

In this example I use a functional React component, but the same applies to a class-based component.

TypeScript Version:

Nightly

Search Terms:

type-guard function parameter narrowing

Code

import React from "react"

interface CommonProps {
    commonProp: 42;
}

interface IOSProps extends CommonProps {
    iOSProp: 42;
    __platform_ios?: never
}

interface AndroidProps extends CommonProps {
    androidProp: 42;
    __platform_android?: never
}

function SomeComponent(props: IOSProps | AndroidProps) {
    return <div/>
}

type ExtractIOS<C> = C extends React.ComponentType<infer R>
  ? R extends { __platform_ios?: never }
    ? React.ComponentType<Extract<R, { __platform_ios?: never }>>
    : never
  : never

function onIOS<C extends React.ComponentType<any>>(
    component: C
): component is ExtractIOS<C> {
    return true // In reality, this would check if the current React Native platform is iOS.
}

if (onIOS(SomeComponent)) {
    (<SomeComponent commonProp={42} iOSProp={42} />);
    // We want this to fail
    (<SomeComponent commonProp={42} androidProp={42} />);
}

Expected behavior:

For it to work similarly when using re-assignment instead of a type-guard:

function makeIOS<C extends React.ComponentType<any>>(
    component: C
): ExtractIOS<C> {
    return component as any
}

const IOSComponent = makeIOS(SomeComponent);
(<IOSComponent commonProp={42} iOSProp={42} />);
// This fails as expected
(<IOSComponent commonProp={42} androidProp={42} />);

Actual behavior:

Type of SomeComponent remains function SomeComponent(props: IOSProps | AndroidProps): JSX.Element after applying the type-guard.

Playground Link:

Link

Question

All 3 comments

This one's a little tricky. The problem is that because parameters are compared contravariantly, you're trying to narrow down a subtype (which is "narrow") to a supertype (which is "wide"). Narrowing is supposed to make types more capable, so but narrowing SomeComponent from (x: IOSProps | AndroidProps) => Element to (x: IOSProps) => Element is strictly making it take fewer types of values.

I think you want SomeComponent itself to be a union, not just the props: playground

Ah yes, nice one 👌 Thanks, this solves the actual problem I had, so this can be closed as far as I am concerned.

Was this page helpful?
0 / 5 - 0 ratings