Yvo
Yvo

Reputation: 19263

TypeScript define generic output type based on input type

For my React-Native project, I'm trying to get as much autocomplete and type validation as I can. One of the challenges is to configure the types for the stylesheet library I use.

An extended stylesheet looks like this:

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

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

When I define the styles, every style value should not only accept its original type, but also a string, function etc.

When the library is done processing the stylesheet, the result is a regular React-Native stylesheet. Therefore the result of the function should contain the same properties as the input of the function, but the properties should be mapped to the original style types.

For example flex should be a number, not a number | string | function | etc.

Here is what I've got so far:

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>

Unfortunately autocomplete doesn't work completely and I feel that my definitions are becoming a bit too complex. The result type also isn't completely correct.

I really hope that there is a TypeScript wizard out there that can help me to get this working.

I've set up a Sandbox that can be used to test some of the types:
https://codesandbox.io/s/typescript-style-mania-h62cv

Upvotes: 3

Views: 1103

Answers (1)

satanTime
satanTime

Reputation: 13574

please let me know if it's a right direction:

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);

Upvotes: 1

Related Questions