Может ли TypeScript определить свойства объекта? - PullRequest
1 голос
/ 29 октября 2019

Я пытаюсь определить определяемый пользователем тип защиты , чтобы проверить, имеет ли переменная типа unknown свойство data. Мой код указан ниже.

type DataObject = {
  data: string
};

function hasDataProperty(myObject: unknown): myObject is DataObject {
    return typeof myObject === "object" &&
        !!myObject &&
        typeof myObject.data === "string";
}

Попробуйте его на игровой площадке Typescript.

Если вы откроете игровую площадку, в строке 8 вы увидите ошибку, указывающуюProperty 'data' does not exist on type 'object'., что имеет смысл, потому что я не проверил, что свойство data существует в myObject.

Я пытался использовать оба "data" in myObject и myObject.hasOwnProperty("data"), чтобы проверить, является ли свойствосуществует, но ни один из них не влияет на предполагаемый тип TypeScript myObject, включая data, это все еще обычный тип object.

Я мог бы изменить сигнатуру функции на hasDataProperty(myObject: any) или использовать типутверждение изменить тип с unknown, но обе эти опции игнорируют фактические свойства объекта, что может привести к ошибкам в логике охранника типа.

Есть ли способ определить, имеет ли myObjectdata свойство без использования утверждений типа или any?

Ответы [ 4 ]

2 голосов
/ 30 октября 2019

Проблема, с которой вы столкнулись, заключается в том, что бит typeof myObject === "object" сообщает компилятору, что экземпляр myObject является , по сути, объектом. Таким образом, TypeScript предполагает в остальной части выражения, что myObject имеет тип {}, который явно не является.

Я бы отпустил unknown в этом методе и применил бы KISS:

function isDataObject(myObject: any): myObject is DataObject {
    return myObject && typeof myObject.data === "string";
}

Редактировать после долгих обсуждений с Зеркмсом и Адамом. Ну, я довольно упрямый, поэтому я продолжал выбирать решения zerkms. Оказывается, может быть более безопасным способом создания Type Guards, и я собираюсь предложить его в следующем.

Моя идея состоит в том, чтобы использовать проверки времени компиляции компилятора дляпроверьте «план» типа охранника. Я собираюсь определить ValidatorDefinition следующим образом:

export type ValidatorTypes =
  | "string"
  | "boolean"
  | "number"
  | "array"
  | "string?"
  | "boolean?"
  | "number?";

export type ValidatorDefinition<T> = {
  [key in keyof T]: T[key] extends string
    ? "string" | "string?"
    : T[key] extends number
    ? "number" | "number?"
    : T[key] extends boolean
    ? "boolean" | "boolean?"
    : (T[key] extends Array<infer TArrayItem>
        ? Array<ValidatorDefinition<TArrayItem>>
        : ValidatorTypes);
};

Таким образом, каждое свойство простого типа должно быть связано со строкой, точно определяющей тип проверки.
Затем яСобираемся построить фабрику TypeGuard:

function typeGuardFactory<T>(
  reference: ValidatorDefinition<T>
): (value: any) => value is T {
  const validators: ((propertyValue: any) => boolean)[] = Object.keys(
    reference
  ).map(key => {
    const referenceValue = (<any>reference)[key];
    switch (referenceValue) {
      case "string":
        return v => typeof v[key] === "string";
      case "boolean":
        return v => typeof v[key] === "boolean";
      case "number":
        return v => typeof v[key] === "number";
      case "string?":
        return v => v[key] == null || typeof v[key] === "string";
      case "boolean?":
        return v => v[key] == null || typeof v[key] === "boolean";
      case "number?":
        return v => v[key] == null || typeof v[key] === "number";
      default:
        // we are not accepting null/undefined for empty array... Should decide how to
        // handle/configure the specific case
        if (Array.isArray(referenceValue)) {
          const arrayItemValidator = typeGuardFactory<any>(referenceValue[0]);
          return v => Array.isArray(v[key]) && v[key].every(arrayItemValidator);
        }
        // TODO: handle default case
        return _v => false;
    }
  });
  return (value: T): value is T =>
    (value && validators.every(validator => validator(value))) || false;
}

Пример использования:

type DataObject = {
  data: string
};
const hasDataObject = typeGuardFactory<DataObject>({ data: "string" });

Приятно то, что если вы ошибетесь в определении Validator, вы получите ошибки времени компиляции.

Какой-то простой код модульного тестирования:

const validatorForDataObject = typeGuardFactory({ data: "string" });

const testCases: { value: any; expected: boolean }[] = [
  { value: null, expected: false },
  { value: undefined, expected: false },
  { value: { data: 0 }, expected: false },
  { value: { data: "0" }, expected: true }
];

testCases.forEach(testCase => {
  const testResult = validatorForDataObject(testCase.value);
  if (testResult === testCase.expected) {
    console.info(`Success (with value ${testResult})`, testCase.value);
  } else {
    console.error(`Fail (with value ${testResult})`, testCase.value);
  }
});

interface AnotherType {
  stringProp: string;
  numberProp: number;
  booleanProp: boolean;
  nullableStringProp?: string;
  nullableNumberProp?: number;
  nullableBooleanProp?: boolean;
  arrayProp: {
    data: string;
  }[];
}

const validatorForAnotherType = typeGuardFactory<AnotherType>({
  stringProp: "string",
  numberProp: "number",
  booleanProp: "boolean",
  nullableStringProp: "string?",
  nullableNumberProp: "number?",
  nullableBooleanProp: "boolean?",
  arrayProp: [
    {
      data: "string"
    }
  ]
});

const testCases2: { value: any; expected: boolean }[] = [
  { value: null, expected: false },
  { value: undefined, expected: false },
  { value: { data: 0 }, expected: false },
  {
    value: {
      stringProp: "string",
      numberProp: 1,
      booleanProp: true,
      nullableStringProp: "string",
      nullableNumberProp: 1,
      nullableBooleanProp: false,
      arrayProp: [{ data: "" }]
    },
    expected: true
  },
  {
    value: {
      stringProp: "string",
      numberProp: 1,
      booleanProp: true,
      nullableStringProp: null,
      nullableNumberProp: null,
      nullableBooleanProp: null,
      arrayProp: []
    },
    expected: true
  },
  {
    value: {
      stringProp: "string",
      numberProp: 1,
      booleanProp: true,
      nullableStringProp: "string",
      nullableNumberProp: null,
      nullableBooleanProp: null,
      arrayProp: [{ data: "" }, { data: "" }, { data: 0 }]
    },
    expected: false
  },
  {
    value: {
      stringProp: "string",
      numberProp: 1,
      booleanProp: true,
      nullableStringProp: "string",
      nullableNumberProp: null,
      nullableBooleanProp: null,
      arrayProp: [{ data: "" }, { data: "" }, { data: 0 }]
    },
    expected: false
  },
  {
    value: {
      stringProp: null,
      numberProp: 1,
      booleanProp: true,
      nullableStringProp: null,
      nullableNumberProp: null,
      nullableBooleanProp: null,
      arrayProp: []
    },
    expected: false
  }
];

testCases2.forEach(testCase => {
  const testResult = validatorForAnotherType(testCase.value);
  if (testResult === testCase.expected) {
    console.info(`Success (with value ${testResult})`, testCase.value);
  } else {
    console.error(`Fail (with value ${testResult})`, testCase.value);
  }
});

Самое смешное, что код проверки все еще широко используется any, но все остальное в мире фарфора за пределамиохранник кажется довольно твердым. Это просто набросок, но я думаю, что он показывает некоторый потенциал.

Полный фрагмент .

1 голос
/ 30 октября 2019

Хорошо, после поиска в Google и обсуждения с кем-то еще, я пришел к этому решению:

type DataObject = {
  data: string
};

function isDataObjectLike(v: unknown): v is { [K in keyof DataObject]: unknown } {
  return typeof v === 'object'
    && v !== null
    && 'data' in v;
}

function hasDataProperty(v: unknown): v is DataObject {
  return isDataObjectLike(v)
    && typeof v.data === 'string';
}

машинопись / игра

Не то, чтобы это было значительно лучше, чем у any, но обеспечивает большую безопасность от компилятора.

0 голосов
/ 30 октября 2019

Хотя я ценю ответ zerkms, я бы предпочел иметь общий и безопасный для типов метод, чтобы определить, существует ли параметр в объекте.

Я провел дальнейшее исследование и нашел решение, которое якак лучше. Я обнаружил эту проблему в репозитории TypeScript , в котором есть блок кода для hasKey, безопасный для определения типа способ определения наличия ключа в объекте.

export function hasKey<K extends string>(k: K, o: {}): o is { [_ in K]: {} } {
  return typeof o === 'object' && k in o
}

ХотяЯ бы предпочел использовать оператор in, на данный момент приемлемой альтернативой является hasKey. Вот как я могу использовать его в моем типе guard:

type DataObject = {
  data: string
};

export function hasKey<K extends string>(k: K, o: {}): o is { [_ in K]: {} } {
  return typeof o === 'object' && k in o
}

function hasDataProperty(myObject: unknown): myObject is DataObject {
    return typeof myObject === "object" &&
      !!myObject &&
      hasKey("data", myObject) &&
      typeof myObject.data === "string";
}

// Testing hasKey
const myThing: object = {};
if (hasKey("myVariable", myThing)) {
  console.log(myThing.myVariable); // This does not error
} else {
  console.log(myThing.myVariable); // This errors
}

Странно, вышеупомянутая проблема TypeScript была закрыта и «исправлена», хотя оператор in по-прежнему не действует как type-guardдля объектов. Здесь есть новая проблема, которая, кажется, отслеживает нечто подобное , хотя эта проблема все еще открыта.

0 голосов
/ 29 октября 2019

вы пытались использовать оператор "in"? Док

type DataObject = {
  data: string
};

function hasDataProperty(myObject: any): myObject is DataObject {
    return "data" in myObject
}

let a = {data: 'a'}
if (hasDataProperty(a)) {
  console.log(a.data); //
} else {
  console.log(a.data); //error
}

Похоже, что он точно делает то, что вы хотите, за исключением того, что он не проверяет тип.

Просто чтобы убедиться, что я бы добавил еще одну проверку типавот так:

function hasDataProperty(myObject: any): myObject is DataObject {
    return "data" in myObject && typeof myObject.data === 'string'
}

ОБНОВЛЕНИЕ:

Извините за измену. Теперь с двухступенчатым подходом:

type DataObject = {
  data: string
};

function hasDataProperty(myObject: unknown): myObject is DataObject {
  function hasDataPropertyAny(myObject: any): myObject is DataObject {
    return "data" in myObject && typeof myObject.data === 'string';
  }
  if (!!myObject && typeof myObject === 'object') {
    return hasDataPropertyAny(myObject);
  } 
  return false;
}

const tests = [
  { data: 'foobar' } as unknown,
  { data: 42 } as unknown,
  42,
  null,
  undefined
]
tests.forEach((a: unknown) => {
  if (hasDataProperty(a)) {
    console.log('DataObject value: ', a.data);
  } else {
    console.log('Not a DataObject!')
  }
})
...