簡體   English   中英

TypeScript generics,ZA8CFDE6331BD4B62AC96F8911中的function參數的約束

[英]TypeScript generics, constraint for function paramters in object

我想要完成的事情:

假設有一個類型如下的配置:

type ExmapleConfig = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => null };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: () => null };
};

所以,一般來說,形狀像這樣:

type AdditionalConfigProps = {
    additionalConfigProp?: string;
    // + more additional props that don't have to be optional
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

type ComponentProps = ReservedComponentProps & Record<string, any>;

type Config = {
    [key: string]: {
        Component: (props: PropsShape) => JSX.Element;
    } & AdditionalConfigProps;
};

我想像這樣轉換配置,但是:

  • 保留鍵的硬類型( 'A' | 'B' | 'C'而不是string
  • 保留道具的硬類型( { type: "a"; a: number; b: number }而不是Record<string, any>
  • 確保變換 function 只接受正確的配置,即:
    • 它具有Component屬性,以及AdditionalConfigProps中具有正確類型的所有其他屬性,
    • 它不會接受定義的ComponentAdditionalConfigProps中的任何其他屬性,
    • Component function 必須能夠接受ComponentProps -like object 作為第一個參數,

轉換可能如下所示:


const config = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> };
    B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div>  };
    C: { Component: () => <div>abc</div>  };
};

/*
    Let's say that it will extract Components, and wrap them
    with additional function so void will be returned instead of JSX
*/
const transformedConfig = transformConfig(config);

// typeof transformedConfig
type ResultType = {
    A: (props: { type: "a"; a: number; b: number }) => void;
    B: (props: { type: "b"; a: string; c: number }) => void;
    C: () => void;
};

請注意:

  • 鍵 'A' 的硬類型 | 'B' | 'C' 被保留
  • 保留了“道具”的硬類型

我嘗試過的方法:

import React from "react";

type AdditionalConfigProps = {
    additionalConfigProp?: string;
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

const CORRECT_CONFIG = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null,
        additionalConfigProp: "abc"
    },
    B: { Component: (props: { type: "b"; a: string; c: number }) => null },
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null },
    D: { Component: (props: {}) => null },
    E: { Component: () => null }
};

const BAD_CONFIG = {
    // Missing Component or other required config prop
    A: {},
    // Bad additionalConfigProp
    B: { Component: () => null, additionalConfigProp: 123 },
    // Bad Component
    C: { Component: 123 },
    // Bad component props type
    D: { Component: (props: boolean) => null },
    // Unexpected 'unknownProp'
    E: { Component: () => null, unknownProp: 123 },
    // Bad 'reservedProp'
    F: { Component: (props: { reservedProp: number }) => null }
};

function configParser<
    Keys extends string,
    ComponentPropsMap extends {
        [Key in Keys]: ReservedComponentProps & Record<string, any>;
    }
>(config: {
    [Key in Keys]: {
        Component: (props?: ComponentPropsMap[Keys]) => React.ReactNode;
    } & AdditionalConfigProps;
}) {
    /*
        TODO: Transform config.
        For now we want to make sure that TS is even able to 'see' it correctly.
    */
    return config;
}

/*
    ❌ Throws unexpected type error
*/
const result = configParser(CORRECT_CONFIG);

// Expected typeof result (what I'd want)
type ExpectedResultType = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null;
        additionalConfigProp: "abc";
    };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null };
    D: { Component: (props: {}) => null };
    E: { Component: () => null };
};

/*
    ❌ Should throw type errors, but not the ones it does
*/
configParser(BAD_CONFIG);

當然我可以做這樣的事情:

function configParser<
    Config extends {
        [key: string]: {
            Component: (componentProps: any) => React.ReactNode;
        };
    }
>(config: Config) {
    return config;
}

// No type error, result type as expected
const result = configParser(CORRECT_CONFIG);

但它:

  • 不會驗證componentProps (也許componentProps: Record<string, any> & ReservedComponentProps會,但由於某種原因它不會接受CORRECT_CONFIG
  • 將允許任何其他配置屬性

這是一種可能的方法:

type VerifyConfigElement<T extends AdditionalConfigProps &
{ Component: (props: any) => void }> =
  { [K in Exclude<keyof T, "Component" | keyof AdditionalConfigProps>]: never } &
  {
    Component: (
      props: Parameters<T["Component"]>[0] extends ComponentProps ? any : ComponentProps
    ) => void
  }

declare function transformConfig<
  T extends Record<keyof T, AdditionalConfigProps & { Component: (props: any) => void }>>(
    config: T & { [K in keyof T]: VerifyConfigElement<T[K]> }
  ): { [K in keyof T]: (...args: Parameters<T[K]["Component"]>) => void }

這個想法是:

  • config參數的類型T中使transformConfig()泛型
  • T 約束為一個相對容易編寫的類型,它不會拒絕好的輸入,在這種情況下,它是AdditionalConfigProps & {Component: (props: any) => void}>
  • 通過將其從自身T[K] 映射到相關類型VerifyConfigElement<T[K]>來更徹底地檢查推斷T的每個屬性,其中T[K] extends VerifyConfigElement<T[K]>當且僅當它是一個好的輸入;
  • T計算返回類型,方法是將T的每個屬性映射到 function 類型,其參數通過索引到相應的Component屬性來確定。

VerifyConfigElement<T>類型檢查兩件事:

  • T沒有在AdditionalConfigProps (或"Component" ,當然)中沒有明確提及的任何屬性......它通過將任何此類額外屬性映射為具有never類型來做到這一點,這幾乎肯定會無法進行類型檢查;
  • TComponent方法的第一個參數可以分配給ComponentProps ...它通過映射到any如果是這樣(這將成功)和ComponentProps如果不是(這可能會失敗?function 類型在其輸入參數中是逆變的,所以有這里可能是一些邊緣情況)。

讓我們測試一下:

const config = {
  A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> },
  B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div> },
  C: { Component: () => <div>abc</div> }
};
// typeof transformedConfig
type ResultType = {
  A: (props: { type: "a"; a: number; b: number }) => void;
  B: (props: { type: "b"; a: string; c: number }) => void;
  C: () => void;
};////
const transformedConfig: ResultType = transformConfig(config);

看起來不錯! 對於您的CORRECT_CONFIGBAD_CONFIG ,編譯器分別接受和拒絕它們:

const okay = transformConfig(CORRECT_CONFIG); // okay
const bad = transformConfig(BAD_CONFIG); // error

如預期的。

Playground 代碼鏈接

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM