Skip to content

Commit cf622ed

Browse files
committed
Merge branch 'main' into at/wizard-integration
2 parents b14c974 + bab8e2a commit cf622ed

File tree

5 files changed

+117
-10
lines changed

5 files changed

+117
-10
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/hooks': patch
3+
---
4+
5+
The type of the returned `value` is now inferred from the types of the parameters in `useControlled`

packages/hooks/src/useControlled/useControlled.spec.tsx

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe('packages/hooks/useControlled', () => {
123123
});
124124

125125
describe('Uncontrolled', () => {
126-
test('calling without a value sets value to `initialValue`', () => {
126+
test('calling without a `controlledValue` sets value to `initialValue`', () => {
127127
const {
128128
result: { current },
129129
} = renderUseControlledHook(undefined, () => {}, 'apple');
@@ -271,5 +271,103 @@ describe('packages/hooks/useControlled', () => {
271271
expect(input).toHaveValue('carrot');
272272
});
273273
});
274+
275+
// eslint-disable-next-line jest/no-disabled-tests
276+
describe.skip('types', () => {
277+
test('controlledValue and initial value must be the same type', () => {
278+
useControlled(1, jest.fn(), 42);
279+
// @ts-expect-error
280+
useControlled(1, jest.fn(), 'foo');
281+
});
282+
283+
test('return type is inferred from `controlledValue`', () => {
284+
{
285+
const { value, updateValue } = useControlled(1);
286+
const _N: number = value;
287+
// @ts-expect-error
288+
const _S: string = value;
289+
290+
updateValue(5);
291+
// @ts-expect-error
292+
updateValue('foo');
293+
}
294+
295+
{
296+
const { value, updateValue } = useControlled('hello');
297+
const _S: string = value;
298+
// @ts-expect-error
299+
const _N: number = value;
300+
301+
updateValue('foo');
302+
// @ts-expect-error
303+
updateValue(5);
304+
}
305+
});
306+
307+
test('return type is inferred from `initialValue` if controlledValue is undefined', () => {
308+
const { value, updateValue } = useControlled(undefined, () => {}, 1);
309+
const _N: number = value;
310+
// @ts-expect-error
311+
const _S: string = value;
312+
313+
updateValue(5);
314+
// @ts-expect-error
315+
updateValue('foo');
316+
// @ts-expect-error
317+
updateValue(undefined);
318+
});
319+
320+
test('return type is `undefined` if no initial or controlledValue are provided', () => {
321+
const { value, updateValue } = useControlled(
322+
undefined,
323+
_v => {},
324+
undefined,
325+
);
326+
const _A: undefined = value;
327+
// @ts-expect-error
328+
const _N: number = value;
329+
// @ts-expect-error
330+
const _S: string = value;
331+
332+
updateValue(undefined);
333+
// @ts-expect-error
334+
updateValue(5);
335+
// @ts-expect-error
336+
updateValue('foo');
337+
});
338+
339+
test('return type is explicit if generic param is provided', () => {
340+
{
341+
const { value, updateValue } = useControlled<number>(
342+
undefined,
343+
() => {},
344+
);
345+
const _N: number = value;
346+
// @ts-expect-error
347+
const _S: string = value;
348+
349+
updateValue(5);
350+
// @ts-expect-error
351+
updateValue('foo');
352+
// @ts-expect-error
353+
updateValue(undefined);
354+
}
355+
356+
{
357+
const { value, updateValue } = useControlled<number | undefined>(
358+
undefined,
359+
() => {},
360+
);
361+
const _N: number | undefined = value;
362+
// @ts-expect-error
363+
const _S: string = value;
364+
365+
updateValue(5);
366+
updateValue(undefined);
367+
// @ts-expect-error
368+
updateValue('foo');
369+
}
370+
});
371+
});
274372
});
275373
});

packages/hooks/src/useControlled/useControlled.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import { ControlledReturnObject } from './useControlled.types';
1010
*
1111
* Returns a {@link ControlledReturnObject}
1212
*/
13-
export const useControlled = <T extends any>(
13+
export const useControlled = <T extends any = undefined>(
1414
controlledValue?: T,
15-
onChange?: (val?: T) => void,
15+
onChange?: (val: T) => void,
1616
initialValue?: T,
17-
): ControlledReturnObject<T | undefined> => {
17+
): ControlledReturnObject<T> => {
1818
/**
1919
* isControlled should only be computed once
2020
*/
@@ -23,9 +23,13 @@ export const useControlled = <T extends any>(
2323

2424
/**
2525
* Keep track of the uncontrolled value state internally
26+
*
27+
* Note on type assertion:
28+
* if `controlledValue` is undefined _and_ `initialValue` is also undefined,
29+
* then T is necessarily `undefined`, so asserting `(initialValue as T)` is safe
2630
*/
27-
const [uncontrolledValue, setUncontrolledValue] = useState<T | undefined>(
28-
initialValue,
31+
const [uncontrolledValue, setUncontrolledValue] = useState<T>(
32+
!isUndefined(controlledValue) ? controlledValue : (initialValue as T),
2933
);
3034

3135
/**
@@ -34,7 +38,7 @@ export const useControlled = <T extends any>(
3438
* If the component is controlled, it will return the controlled value.
3539
*/
3640
const value = useMemo(
37-
() => (isControlled ? controlledValue : uncontrolledValue),
41+
() => (isControlled ? (controlledValue as T) : uncontrolledValue),
3842
[isControlled, uncontrolledValue, controlledValue],
3943
);
4044

@@ -45,7 +49,7 @@ export const useControlled = <T extends any>(
4549
*
4650
* onChange callback is called if provided.
4751
*/
48-
const updateValue = (newVal: T | undefined) => {
52+
const updateValue = (newVal: T) => {
4953
if (!isControlled) {
5054
setUncontrolledValue(newVal);
5155
}

packages/hooks/src/useControlled/useControlled.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface ControlledReturnObject<T extends any> {
99
* Either updates the uncontrolled value,
1010
* or calls the provided `onChange` callback
1111
*/
12-
updateValue: (newVal?: T) => void;
12+
updateValue: (newVal: T) => void;
1313

1414
/**
1515
* A setter for the internal value.

packages/hooks/src/useControlledValue/useControlledValue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const useControlledValue = <T>(
1616
controlledValue?: T,
1717
changeHandler?: ChangeEventHandler<any> | null,
1818
initialValue?: T,
19-
): ControlledValueReturnObject<T | undefined> => {
19+
): ControlledValueReturnObject<T> => {
2020
// Use the new useControlled hook under the hood
2121
const { isControlled, value, setUncontrolledValue } = useControlled(
2222
controlledValue,

0 commit comments

Comments
 (0)