3

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

1
  • Hi, I think I can help you. Could you add to the code parts which you would like to get autocompleted? And an example of types you want to pass in and to get out. Because currently the types look overwhelmed. Commented Apr 3, 2020 at 19:46

1 Answer 1

1
+200

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);
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks! Wow that is a big Type tree :). I'll test this out later today. Those "tests" marked as invalid at the bottom should still be invalid. A TextStyle for example doesn't have a resizeMode property, so it should fail.
right, that's what I meant with "it works", I meant it fails where you expect it to fail, and in case of autocomplete "it works" means that now you can autocomplete. the tree is for the depth respect, looks ugly :) true :) but does its job.
The StyleSet would have to be dynamic. It kinda works like CSS, you could define whatever style classes you need. I'm not sure if TypeScript could grab the closest match. So if it has a fontSize then it must be a TextStyle. I wouldn't mind if it would just accept all of the properties of ViewStyle, TextStyle and ImageStyle with autocomplete. Thanks again, I crown thee a TypeScript wizard :)
Yeah, would be a nice feature, unfortunately, currently it's not supported and there's no way to detect the assumed class interface by its key like fontSize. As an option you could add groups like text: {[key: string]? TextStyle}, view: {[key: string]? TextStyle} etc. but I'm not sure if it fits your architecture.
What would be the proper way to create an AnyStyle type that just accepts all the properties of TextStyle, ImageStyle and ViewStyle? I'm not sure that can be done with just a simple | or &, that seems to create an intersection and doesn't really blend the types together. Or are you saying that that is not supported right now?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.