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> =

如何在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> = 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
}