Typescript: define type of a property based on another property's value

Created on 26 Jan 2020  路  2Comments  路  Source: microsoft/TypeScript

Suggestion

allow these kind of types:

type Type<T> = {
    x: keyof T;
    value: T[x]
}

type of property value is based on value inside property x.
T[x] is only allowed when type of x extends keyof T and keyof T extends x (x = keyof T)

Examples && Use Cases

image a use case like this:

type Filter<T> = { 
    fieldName: keyof T;
    operator: "like" | "equal" | "notLike" | "notEqual",
    value: T[keyof T]
}

type Query<Model> = {
    filters: Filter<Model>[]
};

const model = {
    stringProp: "str",
    intergerProp: 123,
    booleanProp: true as const
}

const query: Query<typeof model> = {
    filters: [
        {
            fieldName: "stringProp",
            operator: "equal",
            value: "test"
        }
    ]
}

approach above works but has following problems:

  • if fieldName: "stringProp" type of value should be string but when we pass a number to value it doesn't give an error because typeof model.integerProp is number.
  • if one of the property has an enum type like "option1" | "option2" and another property's type is string, it ignores the enum and it doesn't give a proper autocompletion.

we can do something with functions like this:

const createFilter = <T, K extends keyof T>(obj: T, fieldName: K, value: T[K]): Filter<T,K> => ({
    fieldName,
    value,
})

but the problem with this approach is:

  • calling unnecessary function / not the best interface
  • we must always pass obj that we may have only type of it
  • user of the type may miss the function and use the property directly (which we can create a dummy property like __do_not_use_directly_use_create_filter_instead__ which is... not ideal. )

related stackoverflow question : https://stackoverflow.com/questions/59857131/how-to-define-type-of-a-property-based-on-another-propertys-value/59857346#comment105847035_59857346

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

Most helpful comment

Looks like you want an existential type (#14466) (e.g., Filter<T> for some K extends keyof T). TS doesn't have direct support for arbitrary existential types, but in cases where you're only interested in an enumerable set of types (like keyof T for types T with known literal keys) you can represent this as a union:

type Filter<T> = { [K in keyof T]: {
    fieldName: K;
    operator: "like" | "equal" | "notLike" | "notEqual",
    value: T[K]
} }[keyof T]

Here's how it looks with something like your example:

const model = {
    stringProp: "str",
    hamburgerProp: 123,
    booleanProp: true
}

type FilterForModel = Filter<typeof model>;
/* type FilterForModel = {
    fieldName: "stringProp";
    operator: "like" | "equal" | "notLike" | "notEqual";
    value: string;
} | {
    fieldName: "hamburgerProp";
    operator: "like" | "equal" | "notLike" | "notEqual";
    value: number;
} | {
    fieldName: "booleanProp";
    operator: "like" | "equal" | "notLike" | "notEqual";
    value: boolean;
} */

You can see that Filter<typeof model> is a union of the three types you want to accept and you don't need a feature addition to the language. Is there a different motivating use case for this?

All 2 comments

Looks like you want an existential type (#14466) (e.g., Filter<T> for some K extends keyof T). TS doesn't have direct support for arbitrary existential types, but in cases where you're only interested in an enumerable set of types (like keyof T for types T with known literal keys) you can represent this as a union:

type Filter<T> = { [K in keyof T]: {
    fieldName: K;
    operator: "like" | "equal" | "notLike" | "notEqual",
    value: T[K]
} }[keyof T]

Here's how it looks with something like your example:

const model = {
    stringProp: "str",
    hamburgerProp: 123,
    booleanProp: true
}

type FilterForModel = Filter<typeof model>;
/* type FilterForModel = {
    fieldName: "stringProp";
    operator: "like" | "equal" | "notLike" | "notEqual";
    value: string;
} | {
    fieldName: "hamburgerProp";
    operator: "like" | "equal" | "notLike" | "notEqual";
    value: number;
} | {
    fieldName: "booleanProp";
    operator: "like" | "equal" | "notLike" | "notEqual";
    value: boolean;
} */

You can see that Filter<typeof model> is a union of the three types you want to accept and you don't need a feature addition to the language. Is there a different motivating use case for this?

@jcalz amazing! I didn't know about this.

thank you, no i will close the issue.

Was this page helpful?
0 / 5 - 0 ratings