【问题标题】:Pass implicit keys and values type relationship to TypeScript generic将隐式键和值类型关系传递给 TypeScript 泛型
【发布时间】:2020-12-20 02:03:49
【问题描述】:

目标

注意用助手类/函数替换isInvalid: falsevalidationPending: false

const controlsAccess: {
    senderName: ControlAccess<string>;
    senderPhoneNumber: ControlAccess<number>;
    senderAddress: ControlAccess<Address>
} = {
    senderName: {
        validatablePayload: {
            value: "",
            isInvalid: false,
            validationPending: false
        },
        vueReferenceID: "SENDER_NAME"
    },
    senderPhoneNumber: {
        validatablePayload: {
            value: 0,
            isInvalid: false,
            validationPending: false
        },
        vueReferenceID: "SENDER_EMAIL"
    },
    senderAddress: {
        validatablePayload: {
            value: { city: "", street: "" },
            isInvalid: false,
            validationPending: false
        },
        vueReferenceID: "SENDER_ADDRESS"
    }
};

类型:

type ValidatableVModel<Value> = {
  isInvalid: boolean;
  value: Value;
  validationPending: boolean;
};

type ControlAccess<ValidatablePayload> = {
  validatablePayload: ValidatableVModel<ValidatablePayload>;
  vueReferenceID: string;
};

type Address = {
    city: string;
    street: string;
}

最终的语法如下:

const controlsAccess: {
    senderName: ControlAccess<string>;
    senderPhoneNumber: ControlAccess<number>;
    senderAddress: ControlAccess<Address>
} = generateControlAccessObject({
   senderName: { vueReferenceID: "SENDER_NAME", initialValue: "" },
   senderPhoneNumber: { vueReferenceID: "SENDER_NAME", initialValue: 0 },
   senderAddress: { vueReferenceID: "SENDER_ADDRESS", initialValue: { city: "", street: "" } }
})

JavaScript 解决方案很简单:

function generateControlAccessObject(dynamicData) {
    const accumulatingResult = {};
    for (const [ key, data ] of Object.entries(dynamicData)) {
      accumulatingResult[key] = {
         validatablePayload: {
             value: data.initialValue,
             isInvalid: false,
             validationPending: false
         },
         vueReferenceID: data.vueReferenceID
      }
    }
    return accumulatingResult;
}

console.log(generateControlAccessObject({
   senderName: { vueReferenceID: "SENDER_NAME", initialValue: "" },
   senderPhoneNumber: { vueReferenceID: "SENDER_NAME", initialValue: 0 },
   senderAddress: { vueReferenceID: "SENDER_ADDRESS", initialValue: { city: "", street: "",  } },
}))

现在如何解释 TypeScript 的参数是什么,返回什么?

概念层面的解决方案

让 TypeScript 知道隐含的键和类型关系:

  • 发件人姓名:string
  • 发件人电话号码:number
  • 发件人地址:Address

我的努力

首先controlsAccess__updatedAPI的类型和generateInputsAccessObject返回的类型不兼容。接下来,accumulatingResult 最初为空,与类型注释不匹配。最后,TypeScript 目前还不知道data 里面是什么。

???? Fiddle

const controlsAccess__updatedAPI: {
    senderName: ControlAccess<string>;
    senderPhoneNumber: ControlAccess<number>;
    senderAddress: ControlAccess<Address>
} = generateInputsAccessObject({
   senderName: { vueReferenceID: "SENDER_NAME", initialValue: "" },
   senderPhoneNumber: { vueReferenceID: "SENDER_NAME", initialValue: 0 },
   senderAddress: { vueReferenceID: "SENDER_ADDRESS", initialValue: { city: "", street: "",  } },
})

type DynamicData<Keys extends string, Values> = Record<Keys, SingleControlDynamicData<Values>>;
type SingleControlDynamicData<Values> = { initialValue: Values; vueReferenceID: string; };

function generateInputsAccessObject<Keys extends string, Values>(
    dynamicData: DynamicData<Keys, Values>
): Record<Keys, { initialValue: Values; vueReferenceID: string; }> {

    const accumulatingResult: Record<Keys, { initialValue: string; vueReferenceID: string; }> = {};

    for (const [ key, data ] of Object.entries(dynamicData)) {
      accumulatingResult[key] = {
        validatablePayload: {
          value: data.initialValue,
          isInvalid: false,
          validationPending: false
        },
        vueReferenceID: data.vueReferenceID
      };
    }

    return accumulatingResult;
}

要求

  • 请不要跳过类型注释
  • 请不要anyobject

【问题讨论】:

  • 通过简化您的问题,您将增加获得答案的机会
  • @captain-yossarian,谢谢你的推荐。请您澄清一下哪一部分是不必要的?

标签: typescript


【解决方案1】:

我认为这应该适合你:

function generateInputsAccessObject<DD extends { [K in string]: { initialValue: any, vueReferenceID: string } }>(
    dynamicData: DD
): { [K in keyof DD]: ControlAccess<DD[K]['initialValue']>}  {

    const accumulatingResult = {} as { [K in keyof DD]: ControlAccess<DD[K]['initialValue']>};

    for (const [ key, data ] of Object.entries(dynamicData)) {
      accumulatingResult[key as keyof DD] = {
        validatablePayload: {
          value: data.initialValue,
          isInvalid: false,
          validationPending: false
        },
        vueReferenceID: data.vueReferenceID
      };
    }

    return accumulatingResult;
}

PLAYGROUND

【讨论】:

  • 感谢您的回答!这真的很有效。其他用户注意:虽然这个解决方案包括any,但可以安全地替换为unknown,所以这个解决方案是可以接受的。
【解决方案2】:

规范答案

您要求一个规范的答案 - 我的解释(可能是错误的)是一个可用于各种类似问题的答案,即更通用的方法。但是,您的问题似乎相当具体,因此我将此答案创建为一种通用方法,该方法允许将源数据类型转换为目标数据类型,其中:

  • 源数据类型和目标数据类型不同,但共享用于映射不同类型的通用键值
  • 源可能存在 0..n 个键,目标键必须与源键集匹配
  • 尽可能使用 TypeScript 类型安全(既不使用 'object' 也未使用 'any')

注意事项

我已经简化了您的数据类型,但这种方法可用于各种数据结构。

使用鉴别器字段允许将键紧密绑定到相应的数据类型,但鉴别器值必须作为字符串文字提供,以便 TypeScript 正确地进行类型检查。 请参阅this link 了解更多信息。

虽然您可以使用泛型来描述源数据字段,但这不会针对不匹配的鉴别器和数据类型提供保护 例如为 ISourceField 使用泛型类型(参见下面的 sn-p)将允许这种不匹配(鉴别器说 PHONE,但实际上它是一个 NAME 对象):

type Discriminator = 'NAME' | 'PHONE' | 'ADDRESS';

senderName: { discriminator: 'PHONE', sourceVaryingType: 'John Smith', sourceSameType: 'source field for senderName' },

interface ISourceField<T> {
    discriminator: Discriminator;
    sourceVaryingType: T;
    sourceSameType: string;
}

interface ISourceFields {
    senderName?: ISourceField<string>
    senderPhoneNumber?: ISourceField<number>
    senderAddress?: ISourceField<IAddress>
}

Record 也有同样的问题,因为它会允许鉴别器和 ISourceField 之间不匹配。

代码

interface IAddress {
    city: string;
    street: string;
}

// source data definitions
interface ISourceFieldName {
    discriminator: 'NAME';
    sourceVaryingType: string;
    sourceSameType: string;
}

interface ISourceFieldPhone {
    discriminator: 'PHONE';
    sourceVaryingType: number;
    sourceSameType: string;
}

interface ISourceFieldAddress {
    discriminator: 'ADDRESS';
    sourceVaryingType: IAddress;
    sourceSameType: string;
}

// ? allows source field to be optional (note you can't use ! on an interface)
interface ISourceFields {
    senderName?: ISourceFieldName;
    senderPhoneNumber?: ISourceFieldPhone;
    senderAddress?: ISourceFieldAddress;
}


// destination data definitions
interface IDestFieldName {
    discriminator: 'NAME';
    destVaryingType: string;
    destSameType: string;
}

interface IDestFieldPhone {
    discriminator: 'PHONE';
    destVaryingType: number;
    destSameType: string;
}

interface IDestFieldAddress {
    discriminator: 'ADDRESS';
    destVaryingType: IAddress;
    destSameType: string;
}

// ! allows dest field to be null, i.e. not exist (for a class, ! allows for simpler syntax within the transform() function, compared to using ?)
class DestFields {
    senderName!: IDestFieldName
    senderPhoneNumber!: IDestFieldPhone
    senderAddress!: IDestFieldAddress
}

type DestFragmentValue = DestFields[keyof DestFields];

function createFragment(key: keyof ISourceFields): DestFragmentValue {
    switch (key) {
        case 'senderName': return { discriminator: 'NAME', destSameType: '', destVaryingType: '' };
        case 'senderPhoneNumber': return { discriminator: 'PHONE', destSameType: '', destVaryingType: 0 };
        case 'senderAddress': return { discriminator: 'ADDRESS', destSameType: '', destVaryingType: { city: '', street: '' } };
        default: throw new Error(`Unexpected key value '${key}' in source data`);
    }
}

function transform(source: ISourceFields): DestFields {
    let dest = new DestFields()
    let key: keyof DestFields;
    for (key in source) {
        // create intialized fragment on dest object
        (dest[key] as DestFragmentValue) = createFragment(key);

        // set required values
        let src = source[key];
        if (src != undefined) {
            dest[key].discriminator = src.discriminator;
            dest[key].destVaryingType = src.sourceVaryingType;
            dest[key].destSameType = src.sourceSameType;
        }
    }

    return dest;
}

// run some tests
let testData = new Array<ISourceFields>(4);

testData[0] = {};

testData[1] = {
    senderName: { discriminator: 'NAME', sourceVaryingType: 'John Smith', sourceSameType: 'source field for senderName' },
};

testData[2] = {
    senderName: { discriminator: 'NAME', sourceVaryingType: 'John Smith', sourceSameType: 'source field for senderName' },
    senderPhoneNumber: { discriminator: 'PHONE', sourceVaryingType: 12345678, sourceSameType: 'source field for senderPhoneNumber' },
};

testData[3] = {
    senderName: { discriminator: 'NAME', sourceVaryingType: 'John Smith', sourceSameType: 'source field for senderName' },
    senderPhoneNumber: { discriminator: 'PHONE', sourceVaryingType: 12345678, sourceSameType: 'source field for senderPhoneNumber' },
    senderAddress: { discriminator: 'ADDRESS', sourceVaryingType: { city: 'Dark City', street: '34 Westfield Drive' }, sourceSameType: 'source field for senderAddress' }
};

console.log('***** Begin tests *****');
for (let i = 0; i < testData.length; i++) {
    let result = transform(testData[i]);
    console.log(`Test number ${i} has result: ${JSON.stringify(result)}`);
    console.log();
}
console.log('***** End tests *****');

TS Playground Link

【讨论】:

  • 感谢您的回答!我会赞成你的问题。很抱歉没有传达我需要多少钥匙。
【解决方案3】:

您的数据格式具有三个键:senderName、senderPhoneNumber 和 senderAddress,但是您没有指定这三个键是否始终存在,或者是否可能只有 0..n 个键存在。假设所有三个键始终存在,那么这是一种方法:

interface Address {
    city: string;
    street: string;
}

interface ValidatableVModel<T> {
    isInvalid: boolean;
    value: T;
    validationPending: boolean;
};

type Discriminator = 'SENDER_NAME' | 'SENDER_PHONE_NUMBER' | 'SENDER_ADDRESS';

interface ControlAccess<T> {
    validatablePayload: ValidatableVModel<T>;
    vueReferenceID: Discriminator;
};

interface ControlsAccess {
    senderName: ControlAccess<string>;
    senderPhoneNumber: ControlAccess<number>;
    senderAddress: ControlAccess<Address>
}

const controlsAccessInitialValue: ControlsAccess = {
    senderName: {
        validatablePayload: {
            value: '',
            isInvalid: false,
            validationPending: false
        },
        vueReferenceID: 'SENDER_NAME'
    },
    senderPhoneNumber: {
        validatablePayload: {
            value: 0,
            isInvalid: false,
            validationPending: false
        },
        vueReferenceID: 'SENDER_PHONE_NUMBER'
    },
    senderAddress: {
        validatablePayload: {
            value: { city: '', street: '' },
            isInvalid: false,
            validationPending: false
        },
        vueReferenceID: 'SENDER_ADDRESS'
    }
};

interface SingleControlDynamicData<T> {
    initialValue: T;
    vueReferenceID: Discriminator;
}

type SingleControlDynamicDataTypes = SingleControlDynamicData<string> | SingleControlDynamicData<number> | SingleControlDynamicData<Address>;
type DynamicData = Record<keyof ControlsAccess, SingleControlDynamicDataTypes>;

const testData: DynamicData = {
    senderName: { vueReferenceID: 'SENDER_NAME', initialValue: 'John Smith' },
    senderPhoneNumber: { vueReferenceID: 'SENDER_PHONE_NUMBER', initialValue: 12345678 },
    senderAddress: { vueReferenceID: 'SENDER_ADDRESS', initialValue: { city: 'Dark City', street: '34 Westfield Drive' } }
};

function generateInputsAccessObject(dynamicData: DynamicData): ControlsAccess {

    let accumulatingResult: ControlsAccess = controlsAccessInitialValue;

    let key: keyof ControlsAccess;
    for (key in dynamicData) {
        accumulatingResult[key].validatablePayload.value = dynamicData[key].initialValue;
        accumulatingResult[key].vueReferenceID = dynamicData[key].vueReferenceID;

        // not required since set to false within controlsAccessInitialValue
        //accumulatingResult[key].validatablePayload.isInvalid = false;
        //accumulatingResult[key].validatablePayload.validationPending = false;
    }

    return accumulatingResult;
}

const result = generateInputsAccessObject(testData);
console.log(`result = ${JSON.stringify(result)}`);

TS Playground Link

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-07-07
    • 1970-01-01
    • 2016-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多