Typescript 类型脚本泛型类型谓词
如何在TypeScript中编写泛型类型谓词 在下面的示例中,Typescript 类型脚本泛型类型谓词,typescript,Typescript,如何在TypeScript中编写泛型类型谓词 在下面的示例中,if(shape.kind=='circle')没有将类型缩小为shape/circle/{kind:'circle',radius:number} interface Circle { kind: 'circle'; radius: number; } interface Square { kind: 'square'; size: number; } type Shape<T = string> =
if(shape.kind=='circle')
没有将类型缩小为shape
/circle
/{kind:'circle',radius:number}
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
size: number;
}
type Shape<T = string> = T extends 'circle' | 'square'
? Extract<Circle | Square, { kind: T }>
: { kind: T };
declare const shape: Shape;
if (shape.kind == 'circle') shape.radius;
// error TS2339: Property 'radius' does not exist on type '{ kind: string; }'.
更新1
@jcalz问题是我需要
declare const kind: string;
if (kind != 'circle' && kind != 'square') shape = { kind };
工作。我想使用一个歧视性的联盟,但不能,正如你所指出的那样。如果它是一个有区别的联合,你能写一个泛型类型谓词吗
type Shape<T = string> = Extract<Circle | Square, { kind: T }>;
更新2
@例如,jcalz在运行时是否可以执行与shape.kind==kind
相同的操作
这里有一个更简洁的演示
declare const s: string;
declare const kind: 'circle' | 'square';
declare let shape: 'circle' | 'square';
if (s == kind) shape = s; // Works ✓
if (shape != kind) shape.length; // Works ✓
function isShape1(s: string, kind: 'circle' | 'square') {
return s == kind;
}
if (isShape1(s, kind)) shape = s;
// error TS2322: Type 'string' is not assignable to type '"square" | "circle"'.
// https://github.com/microsoft/TypeScript/issues/16069
function isShape2(
s: string,
kind: 'circle' | 'square'
): s is 'circle' | 'square' {
return s == kind;
}
if (isShape2(s, kind)) shape = s; // Works ✓
if (!isShape2(shape, kind)) shape.length;
// error TS2339: Property 'length' does not exist on type 'never'.
更新3
感谢@jcalz和@KRyan的周到回答@jcalz的解决方案是有希望的,特别是如果我不允许非缩小的情况,而只是解除它(通过重载)
但是,它仍然受(Number.isInteger(),坏事情发生)的影响。考虑下面的例子
function isTriangle<
T,
K extends T extends K ? never : 'equilateral' | 'isosceles' | 'scalene'
>(triangle: T, kind: K): triangle is K & T {
return triangle == kind;
}
declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
declare const kind: 'equilateral' | 'isosceles';
if (!isTriangle(triangle, kind)) {
switch (triangle) {
case 'equilateral':
// error TS2678: Type '"equilateral"' is not comparable to type '"scalene"'.
}
}
函数是三角形的<
T
K延伸T延伸K?从不:“等边”|“等腰”|“不等边”
>(三角形:T,种类:K):三角形是K&T{
返回三角形==种类;
}
声明常量三角形:'等边'|'等腰'|'不等边';
宣布常数种类:'等边'|'等腰';
if(!isTriangle(三角形,种类)){
开关(三角形){
“等边”情况:
//错误TS2678:类型“等边”与类型“不等边”不可比较。
}
}
三角形
永远不会比种类
窄,所以!由于条件类型(关于缩小条件类型),isTriangle(三角形,种类)
将永远不会永远不会
因此,从根本上讲,您的问题在于缩小值并不会因为映射类型或条件类型而缩小其类型。请参阅,并具体解释这不起作用的原因:
如果我读得正确,我认为这是预期的;在一般情况下,foobar
的类型本身并不一定反映foobar
(类型变量)将描述给定实例化的相同类型。例如:
function compare<T>(x: T, y: T) {
if (typeof x === "string") {
y.toLowerCase() // appropriately errors; 'y' isn't suddenly also a 'string'
}
// ...
}
// why not?
compare<string | number>("hello", 100);
即使在这里,您也必须使用假装
技巧—将变量转换为较窄类型的一个版本,然后当假装
为从不
时,您知道原始变量实际上不是较窄类型的一部分。此外,较窄类型必须是形状匹配,因此您可以执行以下操作
class MatchesKind { private 'matches some kind variable': true; }
declare function isTriangle<T, K>(triangle: T, kind: K): triangle is T & K & MatchesKind;
declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
declare const kind: 'equilateral' | 'isosceles';
if (!isTriangle(triangle, kind)) {
switch (triangle) {
case 'equilateral': 'OK';
}
}
else {
if (triangle === 'scalene') {
// ^^^^^^^^^^^^^^^^^^^^^^
// This condition will always return 'false' since the types
// '("equilateral" & MatchesKind) | ("isosceles" & MatchesKind)'
// and '"scalene"' have no overlap.
'error';
}
}
类MatchesKind{private'匹配某种类型的变量:true;}
声明函数为三角形(三角形:T,种类:K):三角形为T&K&MatchesKind;
声明常量三角形:'等边'|'等腰'|'不等边';
宣布常数种类:'等边'|'等腰';
if(!isTriangle(三角形,种类)){
开关(三角形){
案例‘等边’:‘正常’;
}
}
否则{
如果(三角形=='scalene'){
// ^^^^^^^^^^^^^^^^^^^^^^
//此条件将始终返回“false”,因为类型
//“(“等边”和匹配类)|”(“等腰”和匹配类)”
//和“不等边”没有重叠。
“错误”;
}
}
注意,我在这里使用了if
-开关
由于某种原因似乎不起作用,它允许在第二个块中使用case'scalene'
,而没有任何投诉,即使此时的三角形类型应该使这不可能
然而,这似乎是一个非常非常糟糕的设计。这可能只是一个假设的演示场景,但我真的很难确定您为什么要这样设计。您为什么要对照种类
的值检查三角形
,并得出结果,这一点一点都不清楚出现在类型域中,但不会缩小种类
的范围,使您能够真正了解其类型(从而三角形
)。最好先缩小类
,然后用它来缩小三角形
——在这种情况下,你没有问题。你似乎在某个地方颠倒了一些逻辑,我认为打字脚本是合理的,我对此感到不舒服。我当然不舒服。我要谈谈你的“更新2”代码,但是这个建议应该适用于一般问题。我认为这里的主要情况是isShape(s,k)
应该只充当s
上的类型保护,如果s
还没有比k
更窄的类型。否则你就不想要isShape(s,k)
对s
的类型执行任何操作,因为在true
或false
的情况下,都不是隐含的相关内容(或者至少在类型系统中不能表示任何内容)
因此,我的建议是,在“正确”的情况下,该函数只是一个用户定义的类型保护,如下所示:
default:
otherwise(pretend, shapeAndKind.shape.kind);
break;
type Kind = "circle" | "square";
// isShape(s, k) should only act as a type guard if s is not of a narrower type than k
function isShape<K extends Kind, S extends [S] extends [K] ? never : string>(
s: S,
kind: K
): s is S & K;
// otherwise, isShape(s, k) is not a type guard but just a boolean test
function isShape(s: string, kind: Kind): boolean;
function isShape(s: string, kind: Kind): boolean {
return s === kind;
}
我想这涵盖了你所有的用例。这行吗
不过,如果您已经知道s
的类型比k
窄,而不使用isShape(s,k)
则会更简单。当您使用用户定义的类型保护进行可能存在误判的测试时(如果false
返回值并不意味着关于受保护参数类型的任何新信息),您就是在自食其果。上面的重载定义试图使isShape()
当你将它指向你的脚时,解除它的武装,但是对所有相关人员来说,不需要这些东西更容易。当s
比k
宽时,你可以使用isShape(s,k)
,否则只需使用s==k
或其他一些非类型的防护测试
但无论如何,我希望这会有所帮助。祝你好运
更新
你已经把Kind
扩展到了三个文本,现在我发现我关于哪些情况是“正确的”应该缩小的想法并不完全正确。现在我的攻击计划是isTriangle(t,k)
应该是一个规则
function compare<T>(x: T, y: T) {
if (typeof x === "string") {
y.toLowerCase() // appropriately errors; 'y' isn't suddenly also a 'string'
}
// ...
}
// why not?
compare<string | number>("hello", 100);
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
size: number;
}
type Shape<T = string> = T extends 'circle' | 'square'
? Extract<Circle | Square, { kind: T }>
: { kind: T };
declare const s: string;
declare let shape: Shape;
declare function isShapeOfKind<Kind extends string>(
shape: Shape,
kind: Kind,
): shape is Shape<Kind>;
if (s === 'circle' && isShapeOfKind(shape, s)) {
shape.radius;
}
else if (s === 'square' && isShapeOfKind(shape, s)) {
shape.size;
}
else {
shape.kind;
}
interface ShapeMatchingKind<Kind extends string> {
shape: Shape<Kind>;
kind: Kind;
}
interface ShapeMismatchesKind<ShapeKind extends string, Kind extends string> {
shape: Shape<ShapeKind>;
kind: Kind;
}
type ShapeAndKind = ShapeMatchingKind<string> | ShapeMismatchesKind<string, string>;
declare function isShapeOfKind(
shapeAndKind: ShapeAndKind,
): shapeAndKind is ShapeMatchingKind<string>;
const shapeAndKind = { shape, kind: s };
if (isShapeOfKind(shapeAndKind)) {
const pretend = shapeAndKind as ShapeMatchingKind<'circle'> | ShapeMatchingKind<'square'>;
switch (pretend.kind) {
case 'circle':
pretend.shape.radius;
break;
case 'square':
pretend.shape.size;
break;
default:
shapeAndKind.shape.kind;
break;
}
}
function otherwise<R>(_pretend: never, value: R): R {
return value;
}
default:
otherwise(pretend, shapeAndKind.shape.kind);
break;
class MatchesKind { private 'matches some kind variable': true; }
declare function isTriangle<T, K>(triangle: T, kind: K): triangle is T & K & MatchesKind;
declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
declare const kind: 'equilateral' | 'isosceles';
if (!isTriangle(triangle, kind)) {
switch (triangle) {
case 'equilateral': 'OK';
}
}
else {
if (triangle === 'scalene') {
// ^^^^^^^^^^^^^^^^^^^^^^
// This condition will always return 'false' since the types
// '("equilateral" & MatchesKind) | ("isosceles" & MatchesKind)'
// and '"scalene"' have no overlap.
'error';
}
}
type Kind = "circle" | "square";
// isShape(s, k) should only act as a type guard if s is not of a narrower type than k
function isShape<K extends Kind, S extends [S] extends [K] ? never : string>(
s: S,
kind: K
): s is S & K;
// otherwise, isShape(s, k) is not a type guard but just a boolean test
function isShape(s: string, kind: Kind): boolean;
function isShape(s: string, kind: Kind): boolean {
return s === kind;
}
declare const s: string;
declare const kind: Kind;
declare let shape: Kind;
// Use of type guard on string against Kind literal:
if (isShape(s, "circle")) {
const x: "circle" = s; // s is "circle"
} else {
const x: typeof s = "someString"; // s is string
}
// Use of type guard on Kind against Kind literal:
if (isShape(shape, "circle")) {
const x: "circle" = shape; // shape is "circle"
} else {
const x: "square" = shape; // shape is "square"
}
// Use of type guard on string against Kind:
if (isShape(s, kind)) {
const x: Kind = s; // s is Kind
} else {
const x: typeof s = "someString"; // s is string
}
// Use of type guard on Kind against Kind:
if (isShape(shape, kind)) {
const x: Kind = shape; // shape is Kind (no narrowing has taken place)
} else {
const x: Kind = shape; // shape is Kind (no narrowing has taken place)
}
type _NotAUnion<T, U> = T extends any
? [U] extends [T] ? unknown : never
: never;
type IsSingleStringLiteral<
T extends string,
Y = T,
N = never
> = string extends T ? N : unknown extends _NotAUnion<T, T> ? Y : N;
type TriangleKind = "equilateral" | "isosceles" | "scalene";
function isTriangle<K extends IsSingleStringLiteral<K, TriangleKind, never>>(
triangle: string,
kind: K
): triangle is K;
function isTriangle<K extends TriangleKind>(
triangle: string,
kind: K
): triangle is K & { __brand: K };
function isTriangle(triangle: string, kind: TriangleKind): boolean {
return triangle == kind;
}
declare const triangle: "equilateral" | "isosceles" | "scalene";
declare const twoKind: "equilateral" | "isosceles";
declare const allKind: "equilateral" | "isosceles" | "scalene";
declare const s: string;
// Use of type guard on string against TriangleKind literal:
if (isTriangle(s, "equilateral")) {
const x: "equilateral" = s; // s is "equilateral"
} else {
const x: typeof s = "someString"; // s is string
}
// Use of type guard on string against union of two TriangleKind types:
if (isTriangle(s, twoKind)) {
const x: "equilateral" | "isosceles" = s; // s is "equilateral" | "isosceles"
} else {
const x: typeof s = "someString"; // s is still string, no narrowing
}
// Use of type guard on string against TriangleKind:
if (isTriangle(s, allKind)) {
const x: TriangleKind = s; // s is TriangleKind
} else {
const x: typeof s = "someString"; // s is still string, no narrowing
}
// Use of type guard on TriangleKind against TriangleKind literal:
if (isTriangle(triangle, "equilateral")) {
const x: "equilateral" = triangle; // triangle is "equilateral"
} else {
const x: "isosceles" | "scalene" = triangle; // triangle is "isosceles" | "scalene"
}
// Use of type guard on TriangleKind against union of two TriangleKind types:
if (isTriangle(triangle, twoKind)) {
const x: "equilateral" | "isosceles" = triangle; // triangle is "equilateral" | "isosceles"
} else {
const x: typeof triangle = allKind; // triangle is still TriangleKind, no narrowing
}
// Use of type guard on TriangleKind against TriangleKind:
if (isTriangle(triangle, allKind)) {
const x: TriangleKind = triangle; // triangle is TriangleKind
} else {
const x: typeof triangle = allKind; // triangle is still TriangleKind, no narrowing
}