Skip to content

Commit

Permalink
Polish: disallow improper use of from* APIs, closes #73
Browse files Browse the repository at this point in the history
- Lens.fromProp
- Lens.fromProps
- Lens.fromPath
- Lens.fromNullableProp
- Optional.fromNullableProp
- Optional.fromOptionProp
  • Loading branch information
gcanti committed Dec 29, 2018
1 parent b919037 commit 27b587b
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 35 deletions.
151 changes: 149 additions & 2 deletions dtslint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,172 @@ interface Person {
email: Option<string>
}

type FromPropGood = { [K in 'a' | 'b']: number }

interface FromNullablePropGood {
a?: string
}

//
// Lens.fromProp
//

Lens.fromProp<Person, 'name'>('name') // $ExpectType Lens<Person, string>
Lens.fromProp<FromPropGood>()('a') // $ExpectType Lens<FromPropGood, number>

// $ExpectError
Lens.fromProp<Person, 'foo'>(['foo'])
// $ExpectError
Lens.fromProp<Person, 'name'>(['foo'])
// $ExpectError
Lens.fromProp<string[]>()(1)
// $ExpectError
Lens.fromProp<string[], 1>(1)
// $ExpectError
Lens.fromProp<[string, number]>()('1')
// $ExpectError
Lens.fromProp<[string, number], '1'>('1')
// $ExpectError
Lens.fromProp<[string, number]>()(1)
// $ExpectError
Lens.fromProp<[string, number], 1>(1)
// $ExpectError
Lens.fromProp<Record<string, number>>()('foo')
// $ExpectError
Lens.fromProp<Record<number, number>>()(1)
// $ExpectError
Lens.fromProp<Record<string, number>, 'foo'>('foo')
// $ExpectError
Lens.fromProp<{ [key: string]: number }>()('foo')
// $ExpectError
Lens.fromProp<{ [key: string]: number }, 'foo'>('foo')

//
// fromProps
// Lens.fromProps
//

Lens.fromProps<Person>()(['name', 'age']) // $ExpectType Lens<Person, { name: string; age: number; }>
Lens.fromProps<FromPropGood>()(['a', 'b']) // $ExpectType Lens<FromPropGood, { a: number; b: number; }>

// $ExpectError
Lens.fromProps<Person>()(['foo'])
// $ExpectError
Lens.fromProps<string[]>()([0, 1])
// $ExpectError
Lens.fromProps<[string, number]>()([0, 1])
// $ExpectError
Lens.fromProps<Record<string, number>>()(['foo'])
// $ExpectError
Lens.fromProps<{ [key: string]: number }>()(['foo'])

//
// Lens.fromPath
//

interface FromPathGood {
a: {
b: {
c: {
d: number
}
}
}
}

interface FromPathBad {
a: {
b: {
c: {
[key: string]: number
}
d: Array<number>
}
}
}

Lens.fromPath<FromPathGood>()(['a', 'b', 'c', 'd']) // $ExpectType Lens<FromPathGood, number>
Lens.fromPath<FromPathGood, 'a', 'b', 'c', 'd'>(['a', 'b', 'c', 'd'])

// $ExpectError
Lens.fromPath<FromPathBad>()(['a', 'b', 'c', 'foo'])
// $ExpectError
Lens.fromPath<FromPathBad>()(['a', 'b', 'd', 1])
// $ExpectError
Lens.fromPath<FromPathBad, 'a', 'b', 'c', 'foo'>(['a', 'b', 'c', 'foo'])
// $ExpectError
Lens.fromPath<FromPathBad, 'a', 'b', 'd', 1>(['a', 'b', 'd', 1])

//
// fromOptionProps
// Lens.fromNullableProp
//

Lens.fromNullableProp<FromNullablePropGood, string, 'a'>('a', 'foo') // $ExpectType Lens<FromNullablePropGood, string>
Lens.fromNullableProp<FromNullablePropGood>()('a', 'foo') // $ExpectType Lens<FromNullablePropGood, string>

// $ExpectError
Lens.fromNullableProp<Array<string>, string, 1>(1, 'foo')
// $ExpectError
Lens.fromNullableProp<Array<string>>()(1, 'foo')
// $ExpectError
Lens.fromNullableProp<[string, number], string, 0>(0, 'foo')
// $ExpectError
Lens.fromNullableProp<[string, number]>()(0, 'foo')
// $ExpectError
Lens.fromNullableProp<Record<string, number>, number, 'foo'>('foo', 1)
// $ExpectError
Lens.fromNullableProp<Record<string, number>>()('foo', 1)
// $ExpectError
Lens.fromNullableProp<{ [key: string]: number }, number, 'foo'>('foo', 1)
// $ExpectError
Lens.fromNullableProp<{ [key: string]: number }>()('foo', 1)

//
// Optional.fromNullableProp
//

Optional.fromNullableProp<FromNullablePropGood>()('a') // $ExpectType Optional<FromNullablePropGood, string>
Optional.fromNullableProp<FromNullablePropGood, string, 'a'>('a') // $ExpectType Optional<FromNullablePropGood, string>

// $ExpectError
Optional.fromNullableProp<Array<string>>()(1)
// $ExpectError
Optional.fromNullableProp<Array<string>, string, 1>(1)
// $ExpectError
Optional.fromNullableProp<[string, number]>()(1)
// $ExpectError
Optional.fromNullableProp<[string, number], number, 1>(1)
// $ExpectError
Optional.fromNullableProp<Record<string, number>>()('foo')
// $ExpectError
Optional.fromNullableProp<Record<string, number>, number, 'foo'>('foo')
// $ExpectError
Optional.fromNullableProp<{ [key: string]: number }>()('foo')
// $ExpectError
Optional.fromNullableProp<{ [key: string]: number }, number, 'foo'>('foo')

//
// Optional.fromOptionProp
//

Optional.fromOptionProp<Person>('email') // $ExpectType Optional<Person, string>
// $ExpectError
Optional.fromOptionProp<Person>()('name') // 'name' exists but is not of type Option<any>
// $ExpectError
Optional.fromOptionProp<Person>()('foo') // 'foo' is not a property of Person

// $ExpectError
Optional.fromOptionProp<Array<string>>()(1)
// $ExpectError
Optional.fromOptionProp<Array<string>>(1)
// $ExpectError
Optional.fromOptionProp<[string, number]>()(1)
// $ExpectError
Optional.fromOptionProp<[string, number]>(1)
// $ExpectError
Optional.fromOptionProp<Record<string, Option<number>>>()('foo')
// $ExpectError
Optional.fromOptionProp<Record<string, Option<number>>>('foo')
// $ExpectError
Optional.fromOptionProp<{ [key: string]: Option<number> }>()('foo')
// $ExpectError
Optional.fromOptionProp<{ [key: string]: Option<number> }>('foo')
83 changes: 50 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,27 +119,41 @@ export class Iso<S, A> {
}
}

type ObjectProp<S> = number extends keyof S // disallows arrays, tuples and Record<number, _>
? never
: string extends keyof S // disallows Record<string, _>
? never
: keyof S

export interface LensFromPath<S> {
<
K1 extends keyof S,
K2 extends keyof S[K1],
K3 extends keyof S[K1][K2],
K4 extends keyof S[K1][K2][K3],
K5 extends keyof S[K1][K2][K3][K4]
K1 extends ObjectProp<S>,
K2 extends ObjectProp<S[K1]>,
K3 extends ObjectProp<S[K1][K2]>,
K4 extends ObjectProp<S[K1][K2][K3]>,
K5 extends ObjectProp<S[K1][K2][K3][K4]>
>(
path: [K1, K2, K3, K4, K5]
): Lens<S, S[K1][K2][K3][K4][K5]>
<K1 extends keyof S, K2 extends keyof S[K1], K3 extends keyof S[K1][K2], K4 extends keyof S[K1][K2][K3]>(
<
K1 extends ObjectProp<S>,
K2 extends ObjectProp<S[K1]>,
K3 extends ObjectProp<S[K1][K2]>,
K4 extends ObjectProp<S[K1][K2][K3]>
>(
path: [K1, K2, K3, K4]
): Lens<S, S[K1][K2][K3][K4]>
<K1 extends keyof S, K2 extends keyof S[K1], K3 extends keyof S[K1][K2]>(path: [K1, K2, K3]): Lens<S, S[K1][K2][K3]>
<K1 extends keyof S, K2 extends keyof S[K1]>(path: [K1, K2]): Lens<S, S[K1][K2]>
<K1 extends keyof S>(path: [K1]): Lens<S, S[K1]>
<K1 extends ObjectProp<S>, K2 extends ObjectProp<S[K1]>, K3 extends ObjectProp<S[K1][K2]>>(path: [K1, K2, K3]): Lens<
S,
S[K1][K2][K3]
>
<K1 extends ObjectProp<S>, K2 extends ObjectProp<S[K1]>>(path: [K1, K2]): Lens<S, S[K1][K2]>
<K1 extends ObjectProp<S>>(path: [K1]): Lens<S, S[K1]>
}

function lensFromPath(path: Array<any>): any {
const lens = Lens.fromProp<any, any>(path[0])
return path.slice(1).reduce((acc, prop) => acc.compose(Lens.fromProp<any, any>(prop)), lens)
function lensFromPath(path: Array<unknown>): unknown {
const fromProp: any = Lens.fromProp
return path.slice(1).reduce((acc: any, prop) => acc.compose(fromProp(prop)), fromProp(path[0]))
}

function lensFromProp<S, P extends keyof S>(prop: P): Lens<S, S[P]> {
Expand All @@ -163,36 +177,36 @@ export class Lens<S, A> {
static fromPath<S>(): LensFromPath<S>
static fromPath<
S,
K1 extends keyof S,
K2 extends keyof S[K1],
K3 extends keyof S[K1][K2],
K4 extends keyof S[K1][K2][K3],
K5 extends keyof S[K1][K2][K3][K4]
K1 extends ObjectProp<S>,
K2 extends ObjectProp<S[K1]>,
K3 extends ObjectProp<S[K1][K2]>,
K4 extends ObjectProp<S[K1][K2][K3]>,
K5 extends ObjectProp<S[K1][K2][K3][K4]>
>(path: [K1, K2, K3, K4, K5]): Lens<S, S[K1][K2][K3][K4][K5]>
static fromPath<
S,
K1 extends keyof S,
K2 extends keyof S[K1],
K3 extends keyof S[K1][K2],
K4 extends keyof S[K1][K2][K3]
K1 extends ObjectProp<S>,
K2 extends ObjectProp<S[K1]>,
K3 extends ObjectProp<S[K1][K2]>,
K4 extends ObjectProp<S[K1][K2][K3]>
>(path: [K1, K2, K3, K4]): Lens<S, S[K1][K2][K3][K4]>
static fromPath<S, K1 extends keyof S, K2 extends keyof S[K1], K3 extends keyof S[K1][K2]>(
static fromPath<S, K1 extends ObjectProp<S>, K2 extends ObjectProp<S[K1]>, K3 extends ObjectProp<S[K1][K2]>>(
path: [K1, K2, K3]
): Lens<S, S[K1][K2][K3]>
static fromPath<S, K1 extends keyof S, K2 extends keyof S[K1]>(path: [K1, K2]): Lens<S, S[K1][K2]>
static fromPath<S, K1 extends keyof S>(path: [K1]): Lens<S, S[K1]>
static fromPath<S, K1 extends ObjectProp<S>, K2 extends ObjectProp<S[K1]>>(path: [K1, K2]): Lens<S, S[K1][K2]>
static fromPath<S, K1 extends ObjectProp<S>>(path: [K1]): Lens<S, S[K1]>
static fromPath(): any {
return arguments.length === 0 ? lensFromPath : lensFromPath(arguments[0])
}

static fromProp<S>(): <P extends keyof S>(prop: P) => Lens<S, S[P]>
static fromProp<S, P extends keyof S>(prop: P): Lens<S, S[P]>
static fromProp<S>(): <P extends ObjectProp<S>>(prop: P) => Lens<S, S[P]>
static fromProp<S, P extends ObjectProp<S>>(prop: P): Lens<S, S[P]>
static fromProp(): any {
return arguments.length === 0 ? lensFromProp : lensFromProp<any, any>(arguments[0])
}

/** generate a lens from a type and an array of props */
static fromProps<S>(): <P extends keyof S>(props: Array<P>) => Lens<S, { [K in P]: S[K] }> {
static fromProps<S>(): <P extends ObjectProp<S>>(props: Array<P>) => Lens<S, { [K in P]: S[K] }> {
return props => {
const len = props.length
return new Lens(
Expand All @@ -210,8 +224,11 @@ export class Lens<S, A> {
}

/** generate a lens from a type and a prop whose type is nullable */
static fromNullableProp<S>(): <A extends S[K], K extends keyof S>(k: K, defaultValue: A) => Lens<S, NonNullable<S[K]>>
static fromNullableProp<S, A extends S[K], K extends keyof S>(k: K, defaultValue: A): Lens<S, NonNullable<S[K]>>
static fromNullableProp<S>(): <A extends S[K], K extends ObjectProp<S>>(
k: K,
defaultValue: A
) => Lens<S, NonNullable<S[K]>>
static fromNullableProp<S, A extends S[K], K extends ObjectProp<S>>(k: K, defaultValue: A): Lens<S, NonNullable<S[K]>>
static fromNullableProp(): any {
return arguments.length === 0
? lensFromNullableProp
Expand Down Expand Up @@ -403,7 +420,7 @@ function optionalFromNullableProp<S, K extends keyof S>(k: K): Optional<S, NonNu
return new Optional((s: any) => fromNullable(s[k]), a => s => ({ ...s, [k as any]: a }))
}

type OptionPropertyNames<S> = { [K in keyof S]: S[K] extends Option<any> ? K : never }[keyof S]
type OptionPropertyNames<S> = { [K in ObjectProp<S>]: S[K] extends Option<any> ? K : never }[ObjectProp<S>]
type OptionPropertyType<S, K extends OptionPropertyNames<S>> = S[K] extends Option<infer A> ? A : never

function optionalFromOptionProp<S, K extends OptionPropertyNames<S>>(k: K): Optional<S, OptionPropertyType<S, K>> {
Expand All @@ -420,16 +437,16 @@ export class Optional<S, A> {
readonly _tag: 'Optional' = 'Optional'
constructor(readonly getOption: (s: S) => Option<A>, readonly set: (a: A) => (s: S) => S) {}

static fromNullableProp<S>(): <K extends keyof S>(k: K) => Optional<S, NonNullable<S[K]>>
static fromNullableProp<S, A extends S[K], K extends keyof S>(k: K): Optional<S, NonNullable<S[K]>>
static fromNullableProp<S>(): <K extends ObjectProp<S>>(k: K) => Optional<S, NonNullable<S[K]>>
static fromNullableProp<S, A extends S[K], K extends ObjectProp<S>>(k: K): Optional<S, NonNullable<S[K]>>
static fromNullableProp(): any {
return arguments.length === 0 ? optionalFromNullableProp : optionalFromNullableProp<any, any>(arguments[0])
}

static fromOptionProp<S>(): <P extends OptionPropertyNames<S>>(prop: P) => Optional<S, OptionPropertyType<S, P>>
static fromOptionProp<S>(prop: OptionPropertyNames<S>): Optional<S, OptionPropertyType<S, typeof prop>>
static fromOptionProp(): any {
return arguments.length === 0 ? optionalFromOptionProp : optionalFromOptionProp<any, any>(arguments[0])
return arguments.length === 0 ? optionalFromOptionProp : (optionalFromOptionProp as any)(arguments[0])
}

modify(f: (a: A) => A): (s: S) => S {
Expand Down

0 comments on commit 27b587b

Please sign in to comment.