打字稿'条件'类型

Posted

技术标签:

【中文标题】打字稿\'条件\'类型【英文标题】:Typescript 'Conditional' Types打字稿'条件'类型 【发布时间】:2019-05-03 20:15:49 【问题描述】:

我有一个简单的函数,原则上它的工作很简单,但类型描述很差,几乎每次我操作数据时都需要类型断言。

功能:

const fetch_and_encode = <T, E extends Encoded, C>( source, encode, context : 
    source: Fetcher<T | E> | T | E,
    encode?: Encoder<T, E, C>,
    context?: C
): E => 
    let decoded;
    if ( typeof source === 'function' ) 
        decoded = (source as Fetcher<T | E>)();
     else 
        decoded = source;
    
    if ( typeof encode === 'function' ) 
        return encode(decoded as T, context);
     else 
        return decoded as E;
    
;

引用的类型:

export type Encoded = number | string | ArrayBuffer // | Array<Encoded> | Map<string, Encoded>
export type Fetcher<T> = () => T;
export type Encoder<T, E extends Encoded, C> = (decoded: T, context?: C) => E;

它基本上有 2 个变量,sourceencode,每个变量可以有两种有效类型之一,导致 4 种状态。 source 是一个数据或检索数据的函数。 encode 要么是 undefined,要么是转换 source 的“结果”的函数。最后,这种组合必须产生一个(相对)简单类型的值,Encoded

我尝试了几种不同的方式来改进类型定义,但似乎无法避免需要类型断言。这些尝试也是为了加深我对类型系统的理解,就像清理实际定义一样。也就是说,我“感觉”我应该能够足够严格地指定类型以避免类型断言,我想了解如何。

我的第一次尝试,使用联合,似乎并没有真正改进类型定义:

const fetch_and_encode = <T, E extends Encoded, C>( source, encode, context: 
    source: Fetcher<T>;
    encode: Encoder<T, E, C>;
    context?: C;
 | 
    source: Exclude<T, Function>; // T could still be a subtype of Function
    encode: Encoder<T, E, C>;
    context?: C;
 | 
    source: Fetcher<E>;
    encode: undefined;
    context?: any;
 | 
    source: E;
    encode: undefined;
    context?: any;
): E => 
    let decoded;
    if ( typeof source === 'function' ) 
        decoded = (source as Fetcher<T | E>)();
        // decoded = source(); // Cannot invoke an expression whose type lacks a call signature.  Type 'Fetcher<T> |
        //                     // Fetcher<E> | (Exclude<T, Function> & Function)' has no compatible call signatures.
     else 
        decoded = source;
    
    if ( typeof encode === 'function' ) 
        return encode(decoded as T, context);
        // return encode(decoded, context); // Argument of type 'T | E' is not assignable to parameter of type 'T'
     else 
        return decoded as E;
        // return decoded; // Type 'T | E' is not assignable to type 'E'
    
;

然后我尝试使用实际的条件类型,也无济于事:

const fetch_and_encode = <T, E extends Encoded, C>( source, encode, context : 
    source: Fetcher<T | E> | T | E,
    encode: Encoder<T, E, C> | undefined,
    context: C | undefined
 extends  source: infer S, encode: infer N, context?: C 
    ? S extends Function // Ensure S isn't a Function if it also isn't a Fetcher
        ? S extends Fetcher<T | E>
            ? N extends undefined
                ?  source: Fetcher<E>; encode: undefined; context?: any; 
                :  source: Fetcher<T>; encode: Encoder<T, E, C>; context?: C; 
            : never
        : N extends undefined
            ?  source: E; encode: undefined; context?: any; 
            :  source: T; encode: Encoder<T, E, C>; context?: C; 
    : never
): E => 
    let decoded;
    if ( typeof source === 'function' ) 
        decoded = (source as Fetcher<T | E>)();
        // decoded = source(); // Cannot invoke an expression whose type lacks a call signature.  Type 'Fetcher<T> |
        //                     // Fetcher<E> | (T & Function)' has no compatible call signatures.
     else 
        decoded = source;
    
    if ( typeof encode === 'function' ) 
        return encode(decoded as T, context);
        // return encode(decoded, context); // Argument of type 'T | E' is not assignable to parameter of type 'T'
     else 
        return decoded as E;
        // return decoded; // Type 'T | E' is not assignable to type 'E'
    
;

我不知道还能去哪里。

根据suggestion of Ingo Bürk(如下),我尝试了重载,他们解决了原来的问题,但提出了一个让我困惑的新问题:

function fetch_and_encode<T, E extends Encoded, C>( source, encode, context : 
    //   ^^^^^^^^^^^^^^^^ Overload signature is not compatible with function implementation
    source: E;
    encode: undefined;
    context?: any;
): E;
function fetch_and_encode<T, E extends Encoded, C>( source, encode, context : 
    source: Fetcher<E>;
    encode: undefined;
    context?: any;
): E;
function fetch_and_encode<T, E extends Encoded, C>( source, encode, context : 
    source: Fetcher<T>;
    encode: Encoder<T, E, C>;
    context?: C;
): E;
function fetch_and_encode<T, E extends Encoded, C>( source, encode, context : 
    source: Exclude<T, Function>; // T could still be a subtype of Function
    encode: Encoder<T, E, C>;
    context?: C;
): E 
    let decoded;
    if ( typeof source === 'function' ) 
        decoded = source();
     else 
        decoded = source;
    
    if ( typeof encode === 'function' ) 
        return encode(decoded, context);
     else 
        return decoded;
    

如果我将当前(通用)定义添加为默认值,上述错误就会消失,但再次需要类型断言。

【问题讨论】:

重载签名可能是最简单的:typescriptlang.org/docs/handbook/functions.html#overloads 我还没有考虑过。我会试一试(并祈祷它不会太冗长;-) 源参数可能可以在没有重载的情况下处理,但对于编码情况,我认为它可能更容易。我只是在手机上,所以我现在不能自己玩。我相信提香很快就会过来,给你一个他惊人的答案。 过载...改变了问题。我会补充细节。 另一个想法是让编码器成为一个可选参数,默认为标识函数。这使得打字变得微不足道,尽管从技术上讲当然不能回答这个问题。 【参考方案1】:

以下是使用重载的方法。请注意,实际的函数体是无类型的,我找不到让它工作的好方法(并且不确定它是否可能)。但是函数调用输入正确。

function isFetcher<T>(obj: T | Fetcher<T>): obj is Fetcher<T> 
  return typeof obj === "function";


function fetchAndEncode<A extends Encoded>(source: A | Fetcher<A>): A;
function fetchAndEncode<A, B extends Encoded, C>(source: A | Fetcher<A>, encode: Encoder<A, B, C>, context?: C): B;
function fetchAndEncode(source: any, encode?: any, context?: any) 
  const datum = isFetcher(source) ? source() : source;
  return encode ? encode(datum, context) : datum;

这通过了以下类型测试:

let numericValue: number;

fetchAndEncode(numericValue); // number
fetchAndEncode(true); // error
fetchAndEncode(numericValue, val => `Hello $val`); // string
fetchAndEncode(() => numericValue); // number
fetchAndEncode(() => true); // error
fetchAndEncode(() => numericValue, val => `Hello $val`); // string

【讨论】:

也许我的模型需要改进,但我明确需要该函数返回B,因为这在其他地方受到限制(通过Encoded)。 A 可能是未编码的类型,如果此函数的使用者提供了encode @BenjaminRiggs 我没有关注,如果您通过了编码器,此解决方案会返回 B?你能在电话打不通的地方发个电话吗? 如果encode 未定义,那么A 也必须扩展Encoded,而且,根据我的阅读,这个例子并没有强制执行。 @BenjaminRiggs 我明白了,我完全错过了。我更新了我的答案。

以上是关于打字稿'条件'类型的主要内容,如果未能解决你的问题,请参考以下文章

对象打字稿中的条件类型

打字稿:“关注”条件类型

具有默认参数值的打字稿条件返回类型

如何在打字稿中访问嵌套条件类型

为啥即使我有评估数据的条件,打字稿也会抱怨类型?

React 条件渲染组件打字稿