TypeScript definiera generisk outputtyp baserat på inputtyp


För mitt React-Native-projekt försöker jag få så mycket autokomplettering och typkontroll som möjligt. En av utmaningarna är att konfigurera typerna för det stylesheet-bibliotek jag använder.

Ett utökat stilark ser ut så här:

const styles = createStyles({
  variable1: 100,
  variable2: "10rem",

  header: {
    width: "100%",
    height: 40,
    alignContent: "center",
    flex: "$flexAll",
    margin: "$gapMD"
  }
})

När jag definierar stilarna bör varje stilvärde inte bara acceptera sin ursprungliga typ, utan också en string, function osv.

När biblioteket är färdigt med att bearbeta stilmallen är resultatet en vanlig React-Native-stilmall. Därför bör resultatet av funktionen innehålla samma egenskaper som funktionen fick som inmatning, men egenskaperna bör mappas till de ursprungliga stiltyperna.

Till exempel bör flex vara ett number, inte ett number | string | function | etc.

Här är vad jag har hittills:

import { ImageStyle, TextStyle, ViewStyle } from "react-native"
import EStyleSheet from "react-native-extended-stylesheet"

type Function<K> = () => K

type AllStyles = ImageStyle & TextStyle & ViewStyle
type StyleSet<T> = { [P in keyof T]: AllStyles }

type EValue<T> = T | string & {}
type EVariable<K> = EValue<K> | Function<EValue<K>>
type EStyle<T> = { [P in keyof T]: EVariable<T[P]> }

type EAnyStyle = EStyle<ImageStyle> | EStyle<TextStyle> | EStyle<ViewStyle>
type EStyleSet<T> = { [P in keyof T]: number | string | EAnyStyle | EStyleSet<T> }

export const createStyles = <T>(styles: EStyleSet<T>) =>
                            EStyleSheet.create(styles) as StyleSet<T>

Tyvärr fungerar inte autokomplettering helt och jag känner att mina definitioner håller på att bli lite för komplexa. Resultattypen är heller inte helt korrekt.

Jag hoppas verkligen att det finns en TypeScript-trollkarl där ute som kan hjälpa mig att få detta att fungera.

Jag har satt upp en Sandbox som kan användas för att testa några av typerna:
https://codesandbox.io/s/typescript-style-mania-h62cv

Svar #1

vänligen låt mig veta om det är rätt riktning:

import {FlexStyle, ImageStyle, TextStyle, ViewStyle} from './react-native';

///////////////////////////////////////////////////////
// MOCK

const EStyleSheet = { create: obj => obj };

///////////////////////////////////////////////////////
// TYPES

// thanks ts-essentials.
type Primitive = string | number | boolean | bigint | symbol | undefined | null;
type Builtin = Primitive | Function | Date | Error | RegExp;
type ExtendTypes<T, E> = T extends Builtin
    ? T | E
    : T extends Map<infer K, infer V>
        ? Map<ExtendTypes<K, E>, ExtendTypes<V, E>>
        : T extends ReadonlyMap<infer K, infer V>
            ? ReadonlyMap<K, ExtendTypes<V, E>>
            : T extends WeakMap<infer K, infer V>
                ? WeakMap<K, ExtendTypes<V, E>>
                : T extends Set<infer U>
                    ? Set<ExtendTypes<U, E>>
                    : T extends ReadonlySet<infer U>
                        ? ReadonlySet<ExtendTypes<U, E>>
                        : T extends WeakSet<infer U>
                            ? WeakSet<ExtendTypes<U, E>>
                            : T extends Array<infer U>
                                ? Array<ExtendTypes<U, E>>
                                : T extends Promise<infer U>
                                    ? Promise<ExtendTypes<U, E>>
                                    : T extends {}
                                        ? { [K in keyof T]: ExtendTypes<T[K], E> }
                                        : T;


type AllStyles = ImageStyle & TextStyle & ViewStyle;
type StyleSet<T> = Pick<{
  header: ViewStyle;
  font: TextStyle;
}, Extract<'header' | 'font', keyof T>>;

const createStyles = <T extends {
  // I would add precise definition for the properties here too.
  // header?: ExtendTypes<ViewStyle, Function | String>;
  // font?: ExtendTypes<TextStyle, Function | String>;
  [key: string]: string | number | Function | ExtendTypes<AllStyles, Function | String>; // Capital to keep string unions.
}>(styles: T): StyleSet<T> =>
  EStyleSheet.create(styles);

///////////////////////////////////////////////////////
// TEST

const styles = createStyles({
  variable1: 100,
  variable2: "10rem",

  header: {
    width: "100%",
    height: 40,
    alignItems: "flex-start",
    alignSelf: "$alignSelf", // autocomplete, but also allow custom values
    flex: "$flexAll",
    margin: "$gapMD",
    // autocomplete should work here, but doesn't
    // now it works
  },
  font: {
    fontSize: 20
  }
});

const imageStyle: ImageStyle = {
  alignItems: "center"
};

// Valid
console.log("header", styles.header);
console.log("header.fontSize", styles.font.fontSize);
console.log("imageStyle.alignItems", imageStyle.alignItems);

// Invalid: ViewStyle doesn't have textTransform
// now it works
console.log("header.textTransform", styles.header.textTransform);

// Invalid: TextStyle doesn't have resizeMode
// now it works
console.log("font.resizeMode", styles.font.resizeMode);