Skip to content

Commit c28d34d

Browse files
authored
Merge pull request #36 from mojotech/em/constant-inference
Improve constant decoder type inference
2 parents e51c532 + 7c3c88e commit c28d34d

File tree

2 files changed

+35
-75
lines changed

2 files changed

+35
-75
lines changed

src/decoder.ts

Lines changed: 29 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -203,75 +203,37 @@ export class Decoder<A> {
203203
/**
204204
* Decoder primitive that only matches on exact values.
205205
*
206-
* Note that `constant('string to match')` returns a `Decoder<string>` which
207-
* fails if the input is not equal to `'string to match'`. In many cases this
208-
* is sufficient, but in some situations typescript requires that the decoder
209-
* type be a type-literal. In such a case you must provide the type parameter,
210-
* which looks like `constant<'string to match'>('string to match')`.
211-
*
212-
* Providing the type parameter is only necessary for type-literal strings
213-
* and numbers, as detailed by this table:
214-
*
215-
* ```
216-
* | Decoder | Type |
217-
* | ---------------------------- | ---------------------|
218-
* | constant(true) | Decoder<true> |
219-
* | constant(false) | Decoder<false> |
220-
* | constant(null) | Decoder<null> |
221-
* | constant('alaska') | Decoder<string> |
222-
* | constant<'alaska'>('alaska') | Decoder<'alaska'> |
223-
* | constant(50) | Decoder<number> |
224-
* | constant<50>(50) | Decoder<50> |
225-
* | constant([1,2,3]) | Decoder<number[]> |
226-
* | constant<[1,2,3]>([1,2,3]) | Decoder<[1,2,3]> |
227-
* | constant({x: 't'}) | Decoder<{x: string}> |
228-
* | constant<{x: 't'}>({x: 't'}) | Decoder<{x: 't'}> |
229-
* ```
230-
*
231-
*
232-
* One place where this happens is when a type-literal is in an interface:
233-
* ```
234-
* interface Bear {
235-
* kind: 'bear';
236-
* isBig: boolean;
237-
* }
238-
*
239-
* const bearDecoder1: Decoder<Bear> = object({
240-
* kind: constant('bear'),
241-
* isBig: boolean()
242-
* });
243-
* // Type 'Decoder<{ kind: string; isBig: boolean; }>' is not assignable to
244-
* // type 'Decoder<Bear>'. Type 'string' is not assignable to type '"bear"'.
245-
*
246-
* const bearDecoder2: Decoder<Bear> = object({
247-
* kind: constant<'bear'>('bear'),
248-
* isBig: boolean()
249-
* });
250-
* // no compiler errors
251-
* ```
252-
*
253-
* Another is in type-literal unions:
254-
* ```
255-
* type animal = 'bird' | 'bear';
256-
*
257-
* const animalDecoder1: Decoder<animal> = union(
258-
* constant('bird'),
259-
* constant('bear')
260-
* );
261-
* // Type 'Decoder<string>' is not assignable to type 'Decoder<animal>'.
262-
* // Type 'string' is not assignable to type 'animal'.
263-
*
264-
* const animalDecoder2: Decoder<animal> = union(
265-
* constant<'bird'>('bird'),
266-
* constant<'bear'>('bear')
267-
* );
268-
* // no compiler errors
206+
* For primitive values and shallow structures of primitive values `constant`
207+
* will infer an exact literal type:
208+
* ```
209+
* | Decoder | Type |
210+
* | ---------------------------- | ------------------------------|
211+
* | constant(true) | Decoder<true> |
212+
* | constant(false) | Decoder<false> |
213+
* | constant(null) | Decoder<null> |
214+
* | constant(undefined) | Decoder<undefined> |
215+
* | constant('alaska') | Decoder<'alaska'> |
216+
* | constant(50) | Decoder<50> |
217+
* | constant([1,2,3]) | Decoder<[1,2,3]> |
218+
* | constant({x: 't'}) | Decoder<{x: 't'}> |
219+
* ```
220+
*
221+
* Inference breaks on nested structures, which require an annotation to get
222+
* the literal type:
223+
* ```
224+
* | Decoder | Type |
225+
* | -----------------------------|-------------------------------|
226+
* | constant([1,[2]]) | Decoder<(number|number[])[]> |
227+
* | constant<[1,[2]]>([1,[2]]) | Decoder<[1,[2]]> |
228+
* | constant({x: [1]}) | Decoder<{x: number[]}> |
229+
* | constant<{x: [1]}>({x: [1]}) | Decoder<{x: [1]}> |
269230
* ```
270231
*/
271-
static constant(value: true): Decoder<true>;
272-
static constant(value: false): Decoder<false>;
273-
static constant<A>(value: A): Decoder<A>;
274-
static constant(value: any): Decoder<any> {
232+
static constant<T extends string | number | boolean | []>(value: T): Decoder<T>;
233+
static constant<T extends string | number | boolean, U extends [T, ...T[]]>(value: U): Decoder<U>;
234+
static constant<T extends string | number | boolean, U extends Record<string, T>>(value: U): Decoder<U>;
235+
static constant<T>(value: T): Decoder<T>;
236+
static constant(value: any) {
275237
return new Decoder(
276238
(json: unknown) =>
277239
isEqual(json, value)

test/json-decode.test.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,13 @@ describe('unknownJson', () => {
138138

139139
describe('constant', () => {
140140
it('works for string-literals', () => {
141-
const decoder = constant<'zero'>('zero');
141+
const decoder: Decoder<'zero'> = constant('zero');
142142

143143
expect(decoder.run('zero')).toEqual({ok: true, result: 'zero'});
144144
});
145145

146146
it('fails when given two different values', () => {
147-
const decoder = constant<42>(42);
147+
const decoder: Decoder<42> = constant(42);
148148

149149
expect(decoder.run(true)).toMatchObject({
150150
ok: false,
@@ -180,8 +180,7 @@ describe('constant', () => {
180180
});
181181

182182
it('can decode a constant array', () => {
183-
type A = [1, 2, 3];
184-
const decoder: Decoder<A> = constant<A>([1, 2, 3]);
183+
const decoder: Decoder<[1, 2, 3]> = constant([1, 2, 3]);
185184

186185
expect(decoder.run([1, 2, 3])).toEqual({ok: true, result: [1, 2, 3]});
187186
expect(decoder.run([1, 2, 3, 4])).toMatchObject({
@@ -191,8 +190,7 @@ describe('constant', () => {
191190
});
192191

193192
it('can decode a constant object', () => {
194-
type O = {a: true; b: 12};
195-
const decoder: Decoder<O> = constant<O>({a: true, b: 12});
193+
const decoder: Decoder<{a: true; b: 12}> = constant({a: true, b: 12});
196194

197195
expect(decoder.run({a: true, b: 12})).toEqual({ok: true, result: {a: true, b: 12}});
198196
expect(decoder.run({a: true, b: 7})).toMatchObject({
@@ -579,8 +577,8 @@ describe('union', () => {
579577
type C = A | B;
580578

581579
const decoder: Decoder<C> = union(
582-
object({kind: constant<'a'>('a'), value: number()}),
583-
object({kind: constant<'b'>('b'), value: boolean()})
580+
object({kind: constant('a'), value: number()}),
581+
object({kind: constant('b'), value: boolean()})
584582
);
585583

586584
it('can decode a value that matches one of the union types', () => {

0 commit comments

Comments
 (0)