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

Investigate typed match expression #802

Open
ssalbdivad opened this issue Jun 21, 2023 · 2 comments
Open

Investigate typed match expression #802

ssalbdivad opened this issue Jun 21, 2023 · 2 comments

Comments

@ssalbdivad
Copy link
Member

ssalbdivad commented Jun 21, 2023

I often hear about devs using other languages missing match expressions. I'd need to do some investigation around common syntax and use cases, but I could imagine a thin layer around AT providing type-safe and configurable matching. Something like:

const parseValue = match({
    "uuid": (id) => id,
    "string>5": (s) => s.length,
    "number%2": (n) => n / 2,
    "default": false
})

// 32
console.log(parseValue(64))

// typed as Type<(In: string | number) => Out<number>>
const sizeOf = match({
	string: (s) => s.length,
	"integer>=0": (n) => n,
	// this message could be autogenerated from something like default: "throw"
	default: (v) => error(`${v} must be a string or non-negative integer.`)
})

//   6
const size = sizeOf("foobar")

Here's an example of how it could be used with a scope:

// 	can you match, say { a: 1, m: 1 } and bind the properties a m to parameters
// or class C { c: number } and bind c to a parameter

class C {
    declare c: number
}

export const $ = scope({
    SomeObj: {
        a: "1",
        m: "1"
    },
    C: type("instanceof", C)
})

// getSum inferred as (value: {a: 1, m: 1} | C) => number
const getSum = $.match({
    // a, m inferred as 1
    SomeObj: ({ a, m }) => a + m,
    // c inferred as number
    C: ({ c }) => c,
    // could automatically generate a very specific default error messages describing the cases
    default: () => {
        throw new Error()
    }
})

Could use entry syntax for non string serializable types like objects and custom validators, and combine with transformations. It wouldn't really involve any new logic, the types would be very efficient and fully autocompleted. You could even define your own validation keywords and easily use them in your matcher keys.

It could also reuse all the type reduction + auto-discrimination functionality to error on impossible to hit cases and determine if any branches are a match in O(1) time for most unions.

@ssalbdivad
Copy link
Member Author

Messed around with this a bit. Got completions working but only in expressions, and wasn't able to actually get the function inputs to infer. Likely will require similar workarounds to morph/narrow.

type validateCases<cases, $> = {
	-readonly [k in keyof cases as validateDefinition<k, $, {}>]: (
		In: inferTypeRoot<k, $>
	) => unknown
}

export type MatchParser<$> = {
	<const cases>(def: conform<cases, validateCases<cases, $>>): Type<cases, $>
}

declare const match: MatchParser<Ark>

const sizeOf = match({
	number: (n) => n,
	"string|unknown[]": (data) => data.length
})

@ssalbdivad
Copy link
Member Author

ssalbdivad commented Oct 24, 2023

Updated the above example to provide the best completions possible while allowing inference (completions with an expression doesn't seem possible in keys right now):

type validateCases<cases, $> = {
	// adding keyof $ explicitly provides key completions for aliases
	[k in keyof cases | keyof $]?: k extends validateTypeRoot<k, $>
		? (In: inferTypeRoot<k, $>) => unknown
		: never
}

export type MatchParser<$> = {
	<cases>(
		def: conform<cases, validateCases<cases, $>>
	): Type<
		(In: inferTypeRoot<keyof cases, $>) => Out<returnOf<cases[keyof cases]>>,
		$
	>
}

export declare const match: MatchParser<Ark>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Backlog
Development

No branches or pull requests

1 participant