英文:
Complex conditional types in typescript, narrowing, and `extends` keyword
问题
I have searched a lot and read several question to find a solution to my problem but in no vain. Can you help me?!
What I don't understand is that when a type extends boolean
and put in the if statement condition it should be narrowed to true
but TypeScript has a different idea in some situation:
import Select from "react-select";
export interface Option<Value> {
readonly value: Value;
readonly label?: string;
readonly isDisabled?: boolean;
readonly isFixed?: boolean;
}
export type PropValue<Value, IsMulti extends boolean> = IsMulti extends true
? Value[]
: Value;
const ComboBox = <Value, IsMulti extends boolean>(props: {
value?: PropValue<Value, IsMulti>;
options: Option<Value>[];
isMulti: IsMulti;
}) => {
const { value, isMulti, options } = props;
const mapValue = (x?: PropValue<Value, IsMulti>) => {
if (!x) return undefined;
if (isMulti) {
isMulti;
// ??? why isMulti is not of type `true` but rather still `extends boolean`
// ??? x should be an array but doesn't seem to be narrowed as well
return options.filter(({ value }) => x.includes(value));
}
};
return <Select value={mapValue(value)} isMulti={isMulti} />;
};
A more simple scenario will work as expected:
function experimenting<T extends boolean>(x: boolean, y: T) {
if (x) {
x; //: true
}
if (y) {
y; //: true
}
}
- Can you explain
isMulti
in the first scenario didn't get narrowed to justtrue
? - How to fix the code above so that both
isMulti
andx
are narrowed.
英文:
I have searched a lot and read several question to find a solution to my problem but in no vain. Can you help me?!
What I don't understand is that when a type extends boolean
and put in the if statement condition it should be narrowed to true
but TypeScript has a different idea in some situation:
import Select from "react-select";
export interface Option<Value> {
readonly value: Value;
readonly label?: string;
readonly isDisabled?: boolean;
readonly isFixed?: boolean;
}
export type PropValue<Value, IsMulti extends boolean> = IsMulti extends true
? Value[]
: Value;
const ComboBox = <Value, IsMulti extends boolean>(props: {
value?: PropValue<Value, IsMulti>;
options: Option<Value>[];
isMulti: IsMulti;
}) => {
const { value, isMulti, options } = props;
const mapValue = (x?: PropValue<Value, IsMulti>) => {
if (!x) return undefined;
if (isMulti) {
isMulti;
// ??? why isMulti is not of type `true` but rather still `extends boolean`
// ??? x should be an array but doesn't seem to be narrowed as well
return options.filter(({ value }) => x.includes(value));
}
};
return <Select value={mapValue(value)} isMulti={isMulti} />;
};
A more simple scenario will work as expected:
function experimenting<T extends boolean>(x: boolean, y: T) {
if (x) {
x; //: true
}
if (y) {
y; //: true
}
}
-
Can you explain
isMulti
in the first scenario didn't get narrowed to justtrue
? -
How to fix the code above so that both
isMulti
andx
are narrowed.
答案1
得分: 2
T extends boolean
并不意味着 T
等于 boolean
。extends
子句只表示 T
是 boolean
的子集,而编译器无法将类型缩小到仅仅是 true
,因为它不知道它的确切类型。
例如:
never
是一个空集,而 boolean
的子集包括 true
| false
| never
,因为空集也是一个子集。因此,我们可以将 never
传递给期望泛型参数扩展为 boolean
的函数:
const func = <T extends boolean>() => {};
type A = never extends boolean ? true : false; // true
type B = boolean extends never ? true : false; // false
func<never>() // 没有错误
func<boolean>() // 没有错误
func<string>() // 错误
英文:
T extends boolean
doesn't mean that T
is equal to boolean
. The extends clause just means that T
is a subset of the boolean
and the compiler isn't able to narrow the type to just true
since it doesn't know the exact type of it.
Example:
never
is an empty set and the subsets of the boolean
are true
| false
| never
, since an empty set is also a subset. Thus, we can pass never
to the function that expects a generic parameter that extends boolean
:
const func = <T extends boolean>() => {};
type A = never extends boolean ? true : false; // true
type B = boolean extends never ? true : false; // false
func<never>() // no error
func<boolean>() // no error
func<string>() // error
答案2
得分: 0
You can translate the code comments and type annotations as follows:
您可以改为在`ComboBox`和`PropValue`上扩展`true | undefined`,而不是扩展`Boolean`,并使用默认类型`undefined`,这样您就不必传递`isMulti={undefined}`。
并且在`PropValue`中,您可以使用`NonNullable`来缩小范围,以检查它是否为`true`
```tsx
import React from 'react';
import Select from 'react-select';
export interface Option<Value> {
readonly value: Value;
readonly label?: string;
readonly isDisabled?: boolean;
readonly isFixed?: boolean;
}
export type PropValue<
Value,
IsMulti extends true | undefined
// ^^^^^^^^^^^^^^^^^^^^
> = IsMulti extends NonNullable<IsMulti> ? Value[] : Value;
// ^^^^^^^^^^^^^^^^^^^^^^^^
const ComboBox = <Value, IsMulti extends true | undefined = undefined>(props: {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
value?: PropValue<Value, IsMulti>;
options: Option<Value>[];
isMulti?: IsMulti;
}) => {
const { value, isMulti, options } = props;
const mapValue = (x?: PropValue<Value, IsMulti>) => {
if (!x) return undefined;
if (isMulti && Array.isArray(x)) {
// ^^^^^^^^^^^^^^^
return options.filter(({ value }) => x.includes(value));
}
};
return <Select value={mapValue(value)} isMulti={isMulti} />;
};
export const Test1 = () => {
return (
<ComboBox
value={1}
options={[
{
value: 1,
label: 'label 1',
},
{
value: 2,
label: 'label 2',
},
{
value: 3,
label: 'label 3',
},
]}
/>
);
};
export const Test2 = () => {
return (
<ComboBox
isMulti
value={[1, 2, 3]}
options={[
{
value: 1,
label: 'label 1',
},
{
value: 2,
label: 'label 2',
},
{
value: 3,
label: 'label 3',
},
]}
/>
);
};
此外,用于props的联合类型也可以使用
const ComboBox = <Value>(props: {
// ...
value: Value[];
isMulti: true;
} | {
// ...
value: Value;
isMulti?: false;
}) => {
<details>
<summary>英文:</summary>
You can instead of extending `Boolean`, extend `true | undefined` on the `ComboBox` and `PropValue`, and a default type `undefined` so that you don't have to pass `isMulti={undefined}`
And in the `PropValue` you can narrow it down using `NonNullable` to check if it's `true`
```tsx
import React from 'react';
import Select from 'react-select';
export interface Option<Value> {
readonly value: Value;
readonly label?: string;
readonly isDisabled?: boolean;
readonly isFixed?: boolean;
}
export type PropValue<
Value,
IsMulti extends true | undefined
// ^^^^^^^^^^^^^^^^^^^^
> = IsMulti extends NonNullable<IsMulti> ? Value[] : Value;
// ^^^^^^^^^^^^^^^^^^^^^^^^
const ComboBox = <Value, IsMulti extends true | undefined = undefined>(props: {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
value?: PropValue<Value, IsMulti>;
options: Option<Value>[];
isMulti?: IsMulti;
}) => {
const { value, isMulti, options } = props;
const mapValue = (x?: PropValue<Value, IsMulti>) => {
if (!x) return undefined;
if (isMulti && Array.isArray(x)) {
// ^^^^^^^^^^^^^^^
return options.filter(({ value }) => x.includes(value));
}
};
return <Select value={mapValue(value)} isMulti={isMulti} />;
};
export const Test1 = () => {
return (
<ComboBox
value={1}
options={[
{
value: 1,
label: 'label 1',
},
{
value: 2,
label: 'label 2',
},
{
value: 3,
label: 'label 3',
},
]}
/>
);
};
export const Test2 = () => {
return (
<ComboBox
isMulti
value={[1, 2, 3]}
options={[
{
value: 1,
label: 'label 1',
},
{
value: 2,
label: 'label 2',
},
{
value: 3,
label: 'label 3',
},
]}
/>
);
};
Also, a union for props would work
const ComboBox = <Value,>(props: {
// ...
value: Value[];
isMulti: true;
} | {
// ...
value: Value;
isMulti?: false;
}) => {
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论