Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autoinference by optional generic #57277

Closed
6 tasks done
lveillard opened this issue Feb 2, 2024 · 5 comments
Closed
6 tasks done

Autoinference by optional generic #57277

lveillard opened this issue Feb 2, 2024 · 5 comments
Labels
Duplicate An existing issue was already created

Comments

@lveillard
Copy link

πŸ” Search Terms

Autoinference and optional generic

βœ… Viability Checklist

⭐ Suggestion

type ContentType =
 | 'ID'
 | 'TEXT'
 | 'URL'
 | 'NUMBER';
 ...
 
 type ContentTypeMapper = 
 ID: string;
 TEXT: string;
 URL: string;
 NUMBER: number;
 ....
 
 Type Field<T? extends ContentType>{
  contentType: T
  value: () => ContentTypeMapper[T];
 }

The only way to do something like this right now is to explicitly add T (which can perfectly be computed from T as long as Field is a readonly object, or to do all the possible combinations manually, which is tedious (imagine 20 different ContenTypes)

This feature would enable this to be computed directly without the need to add to each instance of Field, as T can be inferred by the value of its path "contentType"

it is not runtime as long as we have the schema with the fields as const and with readonly.

πŸ“ƒ Motivating Example

The one expressed

πŸ’» Use Cases

  1. What do you want to use this for?
  • JSON schemas mainly, as they are not mutable
  1. What shortcomings exist with current approaches? & 3. What workarounds are you using in the meantime?
  • Need to specify the T while it is included already in the object
  • Otherwise need to manually code all the potential cases
  • This is particularly complicated with the inference requireds two or more fields, as all the combinations would needed to be coded
@lveillard lveillard changed the title Autoinference by optional geeneric Autoinference by optional generic Feb 2, 2024
@RyanCavanaugh
Copy link
Member

You didn't really give me any concrete examples to work from, but I think what you want is this?

 type ContentTypeMapper =  {
  ID: string;
  TEXT: string;
  URL: string;
  NUMBER: number;
 }

type Field = MakeContentFields<keyof ContentTypeMapper>;
type MakeContentFields<K extends keyof ContentTypeMapper> = K extends unknown ? {
  contentType: K;
  value: () => ContentTypeMapper[K];
} : never;

function fn(f: Field) { }

// Error, as expected
fn( { contentType: "ID", value: () => 32 });
// OK, as expected
fn( { contentType: "ID", value: () => "foo" });

@lveillard
Copy link
Author

lveillard commented Feb 3, 2024

Thanks for the idea! So not exactly what i'm trying to achieve, it looks more like this:

 type ContentTypeMapper =  {
  ID: string;
  TEXT: string;
  URL: string;
  NUMBER: number;
 }

 type ContentType = keyof ContentTypeMapper;

//pseudo ts, obviously not working
 type Field <T = ContentType> = {
  path: string,
  contentType: T;
  value: ( val: unknown ) => ContentTypeMapper[T]
 }

type Schema = {
  fields: Field[]
}

const mySchema: Schema = {fields: [
  {
    path: "name",
    contentType: 'TEXT',
    value: () => 8 //should trigger error
  },
  {
    path: "age",
    contentType: 'NUMBER',
    value: () => 8 //this is fine
  }
] as const}

The only way I was able to achieve something like this was manually coding all the options and doing a union type, but having 20 elements on the ContentTypeMapper makes it too long and annoying, while a dynamic solution that understands the immutabilty of the schema would do it faster and cleaner.

it s the 3rd time I come into a similar use-case where I need to use the value of one prop to type other prop. Obviously useless at runtime but all these cases happened with immutable schemas and a limited set of options.

Another issue related to this, is that even doing the unions we get some issues, here is the real code of the ugly workaround: https://github.com/Blitzapps/blitz-orm/blob/main/src/types/schema/fields.ts

type StringField = BormField & {
	contentType:
		| 'ID'
		| 'COLOR'
		| 'DATE'
		| 'FILE'
		| 'EMAIL'
		| 'PHONE'
		| 'URL'
		| 'PASSWORD'
		| 'LANGUAGE_TEXT'
		| 'RICH_TEXT'
		| 'TEXT';
	default?: { type: 'fn'; fn: (currentNode: BQLMutationBlock) => string } | { type: 'value'; value: string };
	validations?: {
		enum?: string[];
		unique?: boolean;
		fn?: (value: string) => boolean;
	};
};

type NumberField = BormField & {
	contentType: 'DURATION' | 'HOUR' | 'RATING' | 'CURRENCY' | 'PERCENTAGE' | 'NUMBER_DECIMAL' | 'NUMBER';
	default?: { type: 'fn'; fn: (currentNode: BQLMutationBlock) => number } | { type: 'value'; value: number };
	validations?: {
		enum?: number[];
		unique?: boolean;
		fn?: (value: number) => boolean;
	};
};
//...

type AllDataField = StringField | NumberField | DateField | BooleanField;

export type DataField = BormField & {
	shared?: boolean;
	validations?: {
		required?: boolean;
		unique?: boolean;
	};
	isVirtual?: boolean;
	dbConnectors?: [DBConnector, ...DBConnector[]];
} & AllDataField;

export type ContentType = keyof ContentTypeMapping;

export type ContentTypeMapping = {
	ID: string;
	JSON: unknown;
	COLOR: string;
	BOOLEAN: boolean;
	///...
	}
  1. so we have StringField, NumberField etc that define manually the types of the functions
  2. Then we do the union type AllDataField = StringField | NumberField | DateField | BooleanField;

And this causes an issue with the enums:

{
	contentType: 'TEXT',
	cardinality: 'ONE',
	path: 'requiredOption',
	default: { type: 'value', value: 'a' },
	validations: { required: true, enum: ['a', 'b', 'c'] as string[] },
},

Which forces me to add the as string[] which should be inferred as contentType = "TEXT"

@fatcerberus
Copy link

So you want to be able to write a generic type and then infer the type parameter from the value that's assigned to it in order to properly constrain the allowed values, is that correct?

type Foo<T> = { foo: T, bar: T };
const x: Foo<~> = { foo: 1206, bar: 777 };  // ok
// x :: Foo<number>
const y: Foo<~> = { foo: 42, bar: "wut" };  // error (TS won't make artificial unions)

If so, then this is a duplicate of #47755.

You can accomplish this today using a pattern called a "constrained identity function", e.g.

function makeFoo<T>(x: { foo: T, bar: T }) {
    return x;
}
const x = makeFoo({ foo: 1206, bar: 777 });  // ok
const y = makeFoo({ foo: 42, bar: "wut" });  // error

but of course it would be nice not to have to write the useless do-nothing function.

@lveillard
Copy link
Author

lveillard commented Feb 3, 2024

Yes something like that would do, like an "optional" generic inferred from other properties.

Thought about the function version, but as you can see by the example is a schema defined in a json format. I would like to have the users of the ORM only needing to use functions to define actual functions, like default values. While the rest should be plain objects.

Should I close this issue and add some extra detail to #477555?

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Feb 5, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Feb 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants